Skip to content

Commit ce15609

Browse files
committed
Enhance image validation in AwsBedrockService and security.utils to support HEIC/HEIF formats, update error messages, and add new test cases for JPEG and HEIC/HEIF images.
1 parent 680792f commit ce15609

File tree

2 files changed

+69
-10
lines changed

2 files changed

+69
-10
lines changed

backend/src/services/aws-bedrock.service.spec.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ vi.mock('@aws-sdk/client-bedrock-runtime', () => {
3434
vi.mock('../utils/security.utils', () => {
3535
return {
3636
validateFileSecurely: vi.fn().mockImplementation((buffer: Buffer, fileType: string) => {
37-
if (!['image/jpeg', 'image/png'].includes(fileType)) {
38-
throw new BadRequestException('Only JPEG and PNG images are allowed');
37+
if (!['image/jpeg', 'image/png', 'image/heic', 'image/heif'].includes(fileType)) {
38+
throw new BadRequestException('Only JPEG, PNG, and HEIC/HEIF images are allowed');
3939
}
4040
}),
4141
sanitizeMedicalData: vi.fn(data => data),
@@ -110,7 +110,7 @@ describe('AwsBedrockService', () => {
110110

111111
describe('extractMedicalInfo', () => {
112112
const mockImageBuffer = Buffer.from('test image content');
113-
const mockImageTypes = ['image/jpeg', 'image/png'];
113+
const mockImageTypes = ['image/jpeg', 'image/png', 'image/heic', 'image/heif'];
114114
const mockMedicalInfo = {
115115
keyMedicalTerms: [
116116
{ term: 'Hemoglobin', definition: 'Protein in red blood cells that carries oxygen' },
@@ -282,10 +282,26 @@ ${JSON.stringify(partialInfo, null, 2)}
282282

283283
it('should reject unsupported file types', async () => {
284284
await expect(service.extractMedicalInfo(mockImageBuffer, 'application/pdf')).rejects.toThrow(
285-
'Only JPEG and PNG images are allowed',
285+
'Only JPEG, PNG, and HEIC/HEIF images are allowed',
286286
);
287287
});
288288

289+
// Add test for mobile phone JPEG with EXIF data
290+
it('should accept JPEG images with EXIF data from mobile phones', async () => {
291+
// Create a mock JPEG buffer with EXIF signature
292+
const mockJpegWithExif = Buffer.from('FFD8FFE1', 'hex');
293+
const result = await service.extractMedicalInfo(mockJpegWithExif, 'image/jpeg');
294+
expect(result).toBeDefined();
295+
});
296+
297+
// Add test for HEIC/HEIF format
298+
it('should accept HEIC/HEIF images from mobile phones', async () => {
299+
// Create a mock HEIC buffer with signature
300+
const mockHeicBuffer = Buffer.from('00000020667479706865696300', 'hex');
301+
const result = await service.extractMedicalInfo(mockHeicBuffer, 'image/heic');
302+
expect(result).toBeDefined();
303+
});
304+
289305
it('should handle errors when image processing fails', async () => {
290306
const error = new Error('Image processing failed');
291307
mockBedrockClient.send.mockRejectedValue(error);

backend/src/utils/security.utils.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,38 @@ const MALICIOUS_FILE_SIGNATURES = new Set([
1212
export const MAX_FILE_SIZES = {
1313
'image/jpeg': 10 * 1024 * 1024,
1414
'image/png': 10 * 1024 * 1024,
15+
'image/heic': 10 * 1024 * 1024,
16+
'image/heif': 10 * 1024 * 1024,
1517
} as const;
1618

1719
// Allowed MIME types
1820
export const ALLOWED_MIME_TYPES = new Set(Object.keys(MAX_FILE_SIZES));
1921

22+
// Common JPEG signatures from different devices
23+
const JPEG_SIGNATURES = new Set([
24+
'FFD8FF', // Standard JPEG SOI marker
25+
'FFD8FFE0', // JPEG/JFIF
26+
'FFD8FFE1', // JPEG/Exif (common in mobile phones)
27+
'FFD8FFE2', // JPEG/SPIFF
28+
'FFD8FFE3', // JPEG/JPEG-LS
29+
'FFD8FFE8', // JPEG/SPIFF
30+
'FFD8FFED', // JPEG/IPTC
31+
'FFD8FFEE', // JPEG/JPEG-LS
32+
]);
33+
34+
// Common PNG signatures
35+
const PNG_SIGNATURES = new Set([
36+
'89504E47', // Standard PNG
37+
'89504E470D0A1A0A', // Full PNG header
38+
]);
39+
40+
// HEIC/HEIF signatures
41+
const HEIC_SIGNATURES = new Set([
42+
'00000020667479706865696300', // HEIC
43+
'0000001C667479706D696631', // HEIF
44+
'00000018667479706D696631', // HEIF variation
45+
]);
46+
2047
/**
2148
* Checks if a buffer starts with any of the malicious file signatures
2249
*/
@@ -30,13 +57,17 @@ const hasExecutableSignature = (buffer: Buffer): boolean => {
3057
* Validates the actual content type of a file using its magic numbers
3158
*/
3259
const validateFileType = (buffer: Buffer, mimeType: string): boolean => {
33-
const signature = buffer.slice(0, 4).toString('hex').toUpperCase();
60+
// Get first 12 bytes to check for various signatures
61+
const signature = buffer.slice(0, 12).toString('hex').toUpperCase();
3462

3563
switch (mimeType) {
3664
case 'image/jpeg':
37-
return signature.startsWith('FFD8FF'); // JPEG SOI marker
65+
return Array.from(JPEG_SIGNATURES).some(sig => signature.startsWith(sig));
3866
case 'image/png':
39-
return signature === '89504E47'; // PNG signature
67+
return Array.from(PNG_SIGNATURES).some(sig => signature.startsWith(sig));
68+
case 'image/heic':
69+
case 'image/heif':
70+
return Array.from(HEIC_SIGNATURES).some(sig => signature.startsWith(sig));
4071
default:
4172
return false;
4273
}
@@ -72,7 +103,7 @@ const calculateEntropy = (buffer: Buffer): number => {
72103
export const validateFileSecurely = (buffer: Buffer, mimeType: string): void => {
73104
// 1. Check if file type is allowed
74105
if (!ALLOWED_MIME_TYPES.has(mimeType)) {
75-
throw new BadRequestException('Only JPEG and PNG images are allowed');
106+
throw new BadRequestException('Only JPEG, PNG, and HEIC/HEIF images are allowed');
76107
}
77108

78109
// 2. Check file size
@@ -117,21 +148,33 @@ const validateImageStructure = (buffer: Buffer): void => {
117148
throw new Error('File too small to be a valid image');
118149
}
119150

151+
const signature = buffer.slice(0, 12).toString('hex').toUpperCase();
152+
120153
// For JPEG
121-
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
154+
if (Array.from(JPEG_SIGNATURES).some(sig => signature.startsWith(sig))) {
122155
// Check for JPEG end marker
123156
if (!(buffer[buffer.length - 2] === 0xff && buffer[buffer.length - 1] === 0xd9)) {
124157
throw new Error('Invalid JPEG structure');
125158
}
126159
}
127160
// For PNG
128-
else if (buffer.slice(0, 8).toString('hex').toUpperCase() === '89504E470D0A1A0A') {
161+
else if (signature.startsWith('89504E47')) {
129162
// Check for IEND chunk
130163
const iendBuffer = Buffer.from([0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]);
131164
if (!buffer.slice(-8).equals(iendBuffer)) {
132165
throw new Error('Invalid PNG structure');
133166
}
134167
}
168+
// For HEIC/HEIF
169+
else if (Array.from(HEIC_SIGNATURES).some(sig => signature.startsWith(sig))) {
170+
// HEIC/HEIF validation is more complex, we'll do basic size validation
171+
if (buffer.length < 512) {
172+
// HEIC files are typically larger
173+
throw new Error('Invalid HEIC/HEIF structure');
174+
}
175+
} else {
176+
throw new Error('Unsupported image format');
177+
}
135178
};
136179

137180
/**

0 commit comments

Comments
 (0)