Skip to content

chore(common): Backport FileTypeValidator fallback support to v10.4.17 #15003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 10.4.17
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions packages/common/pipes/file/file-type.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export type FileTypeValidatorOptions = {
* @default false
*/
skipMagicNumbersValidation?: boolean;

/**
* If `true`, and magic number check fails, fallback to mimetype comparison.
* @default false
*/
fallbackToMimetype?: boolean;
};

/**
Expand All @@ -26,10 +32,26 @@ export class FileTypeValidator extends FileValidator<
IFile
> {
buildErrorMessage(file?: IFile): string {
const expected = this.validationOptions.fileType;

if (file?.mimetype) {
return `Validation failed (current file type is ${file.mimetype}, expected type is ${this.validationOptions.fileType})`;
const baseMessage = `Validation failed (current file type is ${file.mimetype}, expected type is ${expected})`;

/**
* If fallbackToMimetype is enabled, this means the validator failed to detect the file type
* via magic number inspection (e.g. due to an unknown or too short buffer),
* and instead used the mimetype string provided by the client as a fallback.
*
* This message clarifies that fallback logic was used, in case users rely on file signatures.
*/
if (this.validationOptions.fallbackToMimetype) {
return `${baseMessage} - magic number detection failed, used mimetype fallback`;
}

return baseMessage;
}
return `Validation failed (expected type is ${this.validationOptions.fileType})`;

return `Validation failed (expected type is ${expected})`;
}

async isValid(file?: IFile): Promise<boolean> {
Expand All @@ -39,15 +61,14 @@ export class FileTypeValidator extends FileValidator<

const isFileValid = !!file && 'mimetype' in file;

// Skip magic number validation if set
if (this.validationOptions.skipMagicNumbersValidation) {
return (
isFileValid && !!file.mimetype.match(this.validationOptions.fileType)
);
}

if (!isFileValid || !file.buffer) {
return false;
}
if (!isFileValid || !file.buffer) return false;

try {
const { fileTypeFromBuffer } = (await eval(
Expand All @@ -56,9 +77,20 @@ export class FileTypeValidator extends FileValidator<

const fileType = await fileTypeFromBuffer(file.buffer);

return (
!!fileType && !!fileType.mime.match(this.validationOptions.fileType)
);
if (fileType) {
// Match detected mime type against allowed type
return !!fileType.mime.match(this.validationOptions.fileType);
}

/**
* Fallback logic: If file-type cannot detect magic number (e.g. file too small),
* Optionally fall back to mimetype string for compatibility.
* This is useful for plain text, CSVs, or files without recognizable signatures.
*/
if (this.validationOptions.fallbackToMimetype) {
return !!file.mimetype.match(this.validationOptions.fileType);
}
return false;
} catch {
return false;
}
Expand Down
59 changes: 59 additions & 0 deletions packages/common/test/pipes/file/file-type.validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,36 @@ describe('FileTypeValidator', () => {

expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});

it('should return true when fallbackToMimetype is enabled and mimetype matches', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'text/plain',
fallbackToMimetype: true,
});

const shortText = Buffer.from('ok');
const requestFile = {
mimetype: 'text/plain',
buffer: shortText,
} as IFile;

expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
});

it('should return false when fallbackToMimetype is enabled but mimetype does not match', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'application/json',
fallbackToMimetype: true,
});

const shortText = Buffer.from('ok');
const requestFile = {
mimetype: 'text/plain',
buffer: shortText,
} as IFile;

expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});
});

describe('buildErrorMessage', () => {
Expand Down Expand Up @@ -279,5 +309,34 @@ describe('FileTypeValidator', () => {
'Validation failed (current file type is image/png, expected type is jpeg)',
);
});

it('should return false for text file with small buffer and correct mimetype but fail magic number validation', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'text/plain',
});

const textBuffer = Buffer.from('hi'); // too short to identify
const requestFile = {
mimetype: 'text/plain',
buffer: textBuffer,
} as IFile;

expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});

it('should fail validation for text/csv when magic number detection is enabled', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'text/csv',
skipMagicNumbersValidation: false,
});

const csvFile = Buffer.from('name,age\nJohn,30');
const requestFile = {
mimetype: 'text/csv',
buffer: csvFile,
} as IFile;

expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});
});
});