Compare commits

..

No commits in common. "fix/mcp-attachment-validation" and "main" have entirely different histories.

5 changed files with 37 additions and 63 deletions

View file

@ -35,9 +35,19 @@ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/n
import { GetNotificationsDto } from '@gitroom/nestjs-libraries/dtos/notifications/get.notifications.dto';
import { Readable } from 'stream';
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
import { VALID_POST_MEDIA_MIME_TYPES } from '@gitroom/helpers/utils/has.extension';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { fromBuffer } = require('file-type');
const PUBLIC_API_ALLOWED_MIME = new Set<string>([
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/avif',
'image/bmp',
'image/tiff',
'video/mp4',
]);
import * as Sentry from '@sentry/nestjs';
import {
socialIntegrationList,
@ -98,7 +108,7 @@ export class PublicIntegrationsController {
}
const buffer = Buffer.from(await response.arrayBuffer());
const detected = await fromBuffer(buffer);
if (!detected || !VALID_POST_MEDIA_MIME_TYPES.has(detected.mime)) {
if (!detected || !PUBLIC_API_ALLOWED_MIME.has(detected.mime)) {
throw new HttpException({ msg: 'Unsupported file type.' }, 400);
}
const mimetype = detected.mime;

View file

@ -8,26 +8,3 @@ export const hasExtension = (
const ext = extension.startsWith('.') ? extension : `.${extension}`;
return path.toLowerCase().indexOf(ext.toLowerCase()) > -1;
};
const ALLOWED_POST_MEDIA: ReadonlyArray<{ ext: string; mime: string }> = [
{ ext: 'png', mime: 'image/png' },
{ ext: 'jpg', mime: 'image/jpeg' },
{ ext: 'jpeg', mime: 'image/jpeg' },
{ ext: 'gif', mime: 'image/gif' },
{ ext: 'webp', mime: 'image/webp' },
{ ext: 'mp4', mime: 'video/mp4' },
];
export const VALID_POST_MEDIA_EXTENSIONS = ALLOWED_POST_MEDIA.map(
(m) => m.ext
);
export const VALID_POST_MEDIA_MIME_TYPES = new Set<string>(
ALLOWED_POST_MEDIA.map((m) => m.mime)
);
export const isValidPostMediaUrl = (
path: string | undefined | null
): boolean => {
return VALID_POST_MEDIA_EXTENSIONS.some((ext) => hasExtension(path, ext));
};

View file

@ -3,20 +3,25 @@ import {
ValidatorConstraintInterface,
ValidatorConstraint,
} from 'class-validator';
import { VALID_POST_MEDIA_EXTENSIONS } from './has.extension';
@ValidatorConstraint({ name: 'checkValidExtension', async: false })
export class ValidUrlExtension implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
const path = text?.split?.('?')?.[0]?.toLowerCase?.();
if (!path) return false;
return VALID_POST_MEDIA_EXTENSIONS.some((ext) => path.endsWith('.' + ext));
return (
!!text?.split?.('?')?.[0].endsWith('.png') ||
!!text?.split?.('?')?.[0].endsWith('.jpg') ||
!!text?.split?.('?')?.[0].endsWith('.jpeg') ||
!!text?.split?.('?')?.[0].endsWith('.gif') ||
!!text?.split?.('?')?.[0].endsWith('.webp') ||
!!text?.split?.('?')?.[0].endsWith('.mp4')
);
}
defaultMessage(args: ValidationArguments) {
return `File must have a valid extension: ${VALID_POST_MEDIA_EXTENSIONS.map(
(ext) => '.' + ext
).join(', ')}`;
// here you can provide default error message if validation failed
return (
'File must have a valid extension: .png, .jpg, .jpeg, .gif, .webp, or .mp4'
);
}
}

View file

@ -12,10 +12,6 @@ import { Integration } from '@prisma/client';
import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
import { weightedLength } from '@gitroom/helpers/utils/count.length';
import {
isValidPostMediaUrl,
VALID_POST_MEDIA_EXTENSIONS,
} from '@gitroom/helpers/utils/has.extension';
function countCharacters(text: string, type: string): number {
if (type !== 'x') {
@ -134,19 +130,6 @@ If the tools return errors, you would need to rerun it with the right parameters
).id;
const finalOutput = [];
const invalidAttachment = inputData.socialPost
.flatMap((p) => p.postsAndComments)
.flatMap((p) => p.attachments ?? [])
.find((url: string) => !isValidPostMediaUrl(url));
if (invalidAttachment) {
return {
errors: `Attachment "${invalidAttachment}" is not supported. Valid extensions: ${VALID_POST_MEDIA_EXTENSIONS.map(
(ext) => '.' + ext
).join(', ')}.`,
};
}
const integrations = {} as Record<string, Integration>;
for (const platform of inputData.socialPost) {
integrations[platform.integrationId] =

View file

@ -3,16 +3,19 @@ import {
Injectable,
PipeTransform,
} from '@nestjs/common';
import {
VALID_POST_MEDIA_EXTENSIONS,
VALID_POST_MEDIA_MIME_TYPES,
} from '@gitroom/helpers/utils/has.extension';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { fromBuffer } = require('file-type');
const ALLOWED_EXTENSIONS_MESSAGE = `Valid extensions: ${VALID_POST_MEDIA_EXTENSIONS
.map((ext) => '.' + ext)
.join(', ')}`;
const ALLOWED_MIME_TYPES = new Set<string>([
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/avif',
'image/bmp',
'image/tiff',
'video/mp4',
]);
@Injectable()
export class CustomFileValidationPipe implements PipeTransform {
@ -31,10 +34,8 @@ export class CustomFileValidationPipe implements PipeTransform {
}
const detected = await fromBuffer(value.buffer);
if (!detected || !VALID_POST_MEDIA_MIME_TYPES.has(detected.mime)) {
throw new BadRequestException(
`Unsupported file type. ${ALLOWED_EXTENSIONS_MESSAGE}`
);
if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {
throw new BadRequestException('Unsupported file type.');
}
const maxSize = this.getMaxSize(detected.mime);
@ -60,9 +61,7 @@ export class CustomFileValidationPipe implements PipeTransform {
} else if (mimeType.startsWith('video/')) {
return 1024 * 1024 * 1024; // 1 GB
} else {
throw new BadRequestException(
`Unsupported file type. ${ALLOWED_EXTENSIONS_MESSAGE}`
);
throw new BadRequestException('Unsupported file type.');
}
}
}