Files
calendar/packages/lib/server/imageValidation.ts
T
5acdb208f5 feat: enhance image upload validation across the application (#22766)
* feat: enhance image upload validation across the application

* Patch improvements.

* refactor: streamline avatar upload error handling in updateProfile handler

* refactor: extract image validation logic into a separate module for reuse in BannerUploader and ImageUploader

* fix: reset file input value on validation failure in image uploaders

* fix: update localization key for image file upload error message

* fix: update HTML content validation in image uploader to check for additional byte

* Code Quality improvements

* add : unit tests.

* fix : fixing some more issues.

* fix : some import fixes

* fix : fixing type-check issues

* fix : some more import fixes.

* fix : removing Duplicate max-file-size constant

* refactor: enhance base64 validation with regex pattern

* refactor: centralize MAX_IMAGE_FILE_SIZE and fix SVG checks.

* fix : some toast fixes.

* fixing the size of the banner.

* fix: update accepted image formats in BannerUploader and ImageUploader components

* fix

* coderabbit suggestions addressed.

* addressed coderrabit comment

* refactor: implement generic i18n error messages with interpolation

- Add generic 'unsupported_file_type' translation key with {{type}} interpolation
- Replace hardcoded file type error messages with reusable i18n pattern
- Update imageValidation interfaces to support errorKey and errorParams
- Refactor server-side and client-side validation to use new pattern
- Remove individual translation keys for each file type (PDF, HTML, Script, ZIP, Executable)
- Add new translation keys for other validation errors (SVG, base64, empty data, etc.)
- Type-safe error handling with proper interpolation support

Benefits:
- Single reusable translation pattern reduces duplication
- Easier to maintain and localize
- Consistent error messaging across file types

* feat: add MAX_BANNER_SIZE constant and update file size validation

- Add MAX_BANNER_SIZE constant (5MB) to shared constants file
- Update BannerUploader to use shared constant instead of inline value
- Update ImageUploader to handle new errorKey/errorParams pattern
- Maintain backward compatibility with existing error handling

Note: Pre-existing linting warnings in constants.ts and BannerUploader.tsx
are unrelated to these changes and were present before this refactor.

* adressed volnei's suggestions.

* test: update image validation tests for new errorKey/errorParams pattern

- Update all dangerous file type tests to expect errorKey and errorParams instead of hardcoded error messages
- Add comprehensive tests for new error format validation pattern
- Add tests for backward compatibility with error field
- Add tests for MAX_BANNER_SIZE constant integration
- Verify that unsupported file types now return generic 'unsupported_file_type' key with type interpolation
- Ensure all existing functionality continues to work with enhanced error handling

All 42 tests passing 

* test: update server-side image validation tests for new error format

- Update all dangerous file type tests to expect errorKey and errorParams
- Change hardcoded error messages to i18n keys (unsupported_file_type, svg_contains_dangerous_content, etc.)
- Update edge case tests to use new error keys (empty_image_data, invalid_base64_format, unrecognized_image_format)
- Add comprehensive tests for new error format validation pattern
- Add tests for backward compatibility with error field
- Verify that unsupported file types return generic 'unsupported_file_type' key with type interpolation

All 26 server-side tests passing 
Complements client-side test updates for complete test coverage

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2025-09-04 18:22:36 +05:30

133 lines
3.8 KiB
TypeScript

/**
* Server-side image validation utility with magic number checking to prevent XSS attacks
* This prevents malicious files (like PDFs with embedded JavaScript) from being uploaded as images
*/
import {
FILE_SIGNATURES,
matchesSignature,
containsDangerousSVGContent,
isValidBase64,
} from "../imageValidationConstants";
export interface ImageValidationResult {
isValid: boolean;
error?: string;
errorKey?: string;
errorParams?: Record<string, string | number>;
detectedFormat?: string;
}
/**
* Validate base64 image data for magic numbers and content
*/
export function validateBase64Image(base64Data: string): ImageValidationResult {
try {
const base64Content = base64Data.replace(/^data:image\/[^;]+;base64,/, "");
if (!isValidBase64(base64Content)) {
return { isValid: false, error: "invalid_base64_format" };
}
const buffer = Buffer.from(base64Content, "base64");
const uint8Array = new Uint8Array(buffer);
if (uint8Array.length === 0) {
return { isValid: false, error: "empty_image_data" };
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.PDF)) {
return {
isValid: false,
errorKey: "unsupported_file_type",
errorParams: { type: "PDF" },
detectedFormat: "PDF",
};
}
if (
matchesSignature(uint8Array, FILE_SIGNATURES.HTML) ||
matchesSignature(uint8Array, FILE_SIGNATURES.HTML_TAG)
) {
return {
isValid: false,
errorKey: "unsupported_file_type",
errorParams: { type: "HTML" },
detectedFormat: "HTML",
};
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.SCRIPT_TAG)) {
return {
isValid: false,
errorKey: "unsupported_file_type",
errorParams: { type: "Script" },
detectedFormat: "Script",
};
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.ZIP)) {
return {
isValid: false,
errorKey: "unsupported_file_type",
errorParams: { type: "ZIP" },
detectedFormat: "ZIP",
};
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.EXECUTABLE)) {
return {
isValid: false,
errorKey: "unsupported_file_type",
errorParams: { type: "Executable" },
detectedFormat: "Executable",
};
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.PNG)) {
return { isValid: true, detectedFormat: "PNG" };
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.JPEG_FF_D8_FF)) {
return { isValid: true, detectedFormat: "JPEG" };
}
if (
matchesSignature(uint8Array, FILE_SIGNATURES.GIF87a) ||
matchesSignature(uint8Array, FILE_SIGNATURES.GIF89a)
) {
return { isValid: true, detectedFormat: "GIF" };
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.BMP)) {
return { isValid: true, detectedFormat: "BMP" };
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.ICO)) {
return { isValid: true, detectedFormat: "ICO" };
}
if (matchesSignature(uint8Array, FILE_SIGNATURES.WEBP) && uint8Array.length >= 12) {
if (matchesSignature(uint8Array.slice(8), FILE_SIGNATURES.WEBP_SIGNATURE)) {
return { isValid: true, detectedFormat: "WEBP" };
}
}
if (
matchesSignature(uint8Array, FILE_SIGNATURES.SVG) ||
matchesSignature(uint8Array, FILE_SIGNATURES.SVG_DIRECT)
) {
const textContent = buffer.toString("utf8");
if (containsDangerousSVGContent(textContent)) {
return { isValid: false, error: "svg_contains_dangerous_content", detectedFormat: "SVG" };
}
return { isValid: true, detectedFormat: "SVG" };
}
return { isValid: false, error: "unrecognized_image_format", detectedFormat: "Unknown" };
} catch (error) {
return { isValid: false, error: "failed_to_validate_image_file" };
}
}