mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
fix(tests): use valid PNG IHDR chunks in mime-validation fixtures
file-type >=21.3.4 now validates PNG structure beyond just the 8-byte signature. The minimal header+zeros buffer no longer detects as image/png. Updated both the PNG acceptance test and the PNG-as-JPG mismatch test to include a valid IHDR chunk (1x1 RGB pixel), making them compatible with both current and upcoming file-type versions.
This commit is contained in:
parent
a4a506c5c4
commit
9a3f9a930d
6 changed files with 6014 additions and 3001 deletions
|
|
@ -17,7 +17,7 @@
|
|||
"chalk": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"typescript": "^5.7.0",
|
||||
"tsx": "^4.19.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@
|
|||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@veritas-kanban/shared": "workspace:*",
|
||||
"hono": "^4.12.5",
|
||||
"hono": "^4.12.9",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -35,18 +35,18 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^25.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
||||
"@typescript-eslint/parser": "^8.57.2",
|
||||
"concurrently": "^9.1.0",
|
||||
"eslint": "9.38.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.3.2",
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.1.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
|
|
|
|||
8823
pnpm-lock.yaml
generated
8823
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -27,24 +27,24 @@
|
|||
"exceljs": "^4.4.0",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.3.1",
|
||||
"file-type": "^21.3.1",
|
||||
"file-type": "^21.3.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mammoth": "^1.11.0",
|
||||
"mammoth": "^1.12.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"multer": "^2.1.1",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "^5.1.7",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.17.1",
|
||||
"simple-git": "^3.32.3",
|
||||
"sanitize-filename": "^1.6.4",
|
||||
"sanitize-html": "^2.17.2",
|
||||
"simple-git": "^3.33.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"unpdf": "^1.4.0",
|
||||
"ws": "^8.18.0",
|
||||
"yaml": "^2.8.2",
|
||||
"ws": "^8.20.0",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"eslint": "^9.17.0",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.19.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { validateMimeType, ALLOWED_TYPES, getAllowedTypesDescription } from '../services/mime-validation.js';
|
||||
import {
|
||||
validateMimeType,
|
||||
ALLOWED_TYPES,
|
||||
getAllowedTypesDescription,
|
||||
} from '../services/mime-validation.js';
|
||||
|
||||
describe('MIME Validation', () => {
|
||||
describe('Text-based files', () => {
|
||||
|
|
@ -24,7 +28,12 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should accept JSON files', async () => {
|
||||
const buffer = Buffer.from('{"key":"value"}');
|
||||
const result = await validateMimeType(buffer, 'config.json', 'application/json', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'config.json',
|
||||
'application/json',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -36,7 +45,12 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should accept YAML files', async () => {
|
||||
const buffer = Buffer.from('key: value\nlist:\n - item1');
|
||||
const result = await validateMimeType(buffer, 'config.yaml', 'application/yaml', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'config.yaml',
|
||||
'application/yaml',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -49,9 +63,36 @@ describe('MIME Validation', () => {
|
|||
|
||||
describe('Binary files with magic bytes', () => {
|
||||
it('should accept PNG files with correct magic bytes', async () => {
|
||||
// PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A
|
||||
const pngHeader = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
const buffer = Buffer.concat([pngHeader, Buffer.alloc(100)]);
|
||||
// PNG signature + valid IHDR chunk (required by file-type >=21.3.4)
|
||||
const pngHeader = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdrChunk = Buffer.from([
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d, // chunk length: 13
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52, // 'IHDR'
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01, // width: 1
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01, // height: 1
|
||||
0x08,
|
||||
0x02, // bit depth: 8, color type: RGB
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // compression, filter, interlace
|
||||
0x90,
|
||||
0x77,
|
||||
0x53,
|
||||
0xde, // CRC32
|
||||
]);
|
||||
const buffer = Buffer.concat([pngHeader, ihdrChunk, Buffer.alloc(50)]);
|
||||
const result = await validateMimeType(buffer, 'image.png', 'image/png', buffer.length);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.detectedMime).toBe('image/png');
|
||||
|
|
@ -59,7 +100,7 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should accept JPEG files with correct magic bytes', async () => {
|
||||
// JPEG magic bytes: FF D8 FF
|
||||
const jpegHeader = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
const jpegHeader = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const buffer = Buffer.concat([jpegHeader, Buffer.alloc(100)]);
|
||||
const result = await validateMimeType(buffer, 'photo.jpg', 'image/jpeg', buffer.length);
|
||||
expect(result.valid).toBe(true);
|
||||
|
|
@ -79,7 +120,12 @@ describe('MIME Validation', () => {
|
|||
// PDF magic bytes: 25 50 44 46 (%PDF)
|
||||
const pdfHeader = Buffer.from('%PDF-1.4');
|
||||
const buffer = Buffer.concat([pdfHeader, Buffer.alloc(100)]);
|
||||
const result = await validateMimeType(buffer, 'document.pdf', 'application/pdf', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'document.pdf',
|
||||
'application/pdf',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.detectedMime).toBe('application/pdf');
|
||||
});
|
||||
|
|
@ -88,7 +134,12 @@ describe('MIME Validation', () => {
|
|||
describe('Blocked extensions', () => {
|
||||
it('should reject .exe files', async () => {
|
||||
const buffer = Buffer.from('MZ'); // PE header
|
||||
const result = await validateMimeType(buffer, 'malware.exe', 'application/octet-stream', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'malware.exe',
|
||||
'application/octet-stream',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('.exe');
|
||||
expect(result.error).toContain('not allowed');
|
||||
|
|
@ -96,7 +147,12 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should reject .bat files', async () => {
|
||||
const buffer = Buffer.from('@echo off');
|
||||
const result = await validateMimeType(buffer, 'script.bat', 'application/x-bat', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'script.bat',
|
||||
'application/x-bat',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('.bat');
|
||||
});
|
||||
|
|
@ -110,21 +166,36 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should reject .js files', async () => {
|
||||
const buffer = Buffer.from('console.log("pwned")');
|
||||
const result = await validateMimeType(buffer, 'payload.js', 'application/javascript', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'payload.js',
|
||||
'application/javascript',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('.js');
|
||||
});
|
||||
|
||||
it('should reject .dll files', async () => {
|
||||
const buffer = Buffer.from('MZ');
|
||||
const result = await validateMimeType(buffer, 'library.dll', 'application/octet-stream', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'library.dll',
|
||||
'application/octet-stream',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('.dll');
|
||||
});
|
||||
|
||||
it('should reject .php files', async () => {
|
||||
const buffer = Buffer.from('<?php echo "hi"; ?>');
|
||||
const result = await validateMimeType(buffer, 'shell.php', 'application/x-httpd-php', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'shell.php',
|
||||
'application/x-httpd-php',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('.php');
|
||||
});
|
||||
|
|
@ -139,9 +210,36 @@ describe('MIME Validation', () => {
|
|||
|
||||
describe('Extension mismatch detection', () => {
|
||||
it('should reject a PNG file disguised as .jpg if content mismatch is detected', async () => {
|
||||
// PNG magic bytes but with .jpg extension
|
||||
const pngHeader = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
const buffer = Buffer.concat([pngHeader, Buffer.alloc(100)]);
|
||||
// PNG signature + valid IHDR chunk but with .jpg extension
|
||||
const pngHeader = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdrChunk = Buffer.from([
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d, // chunk length: 13
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52, // 'IHDR'
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01, // width: 1
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01, // height: 1
|
||||
0x08,
|
||||
0x02, // bit depth: 8, color type: RGB
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // compression, filter, interlace
|
||||
0x90,
|
||||
0x77,
|
||||
0x53,
|
||||
0xde, // CRC32
|
||||
]);
|
||||
const buffer = Buffer.concat([pngHeader, ihdrChunk, Buffer.alloc(50)]);
|
||||
const result = await validateMimeType(buffer, 'photo.jpg', 'image/jpeg', buffer.length);
|
||||
// Both are images, so this should still be allowed (same category)
|
||||
expect(result.valid).toBe(true);
|
||||
|
|
@ -149,7 +247,7 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should reject executable content disguised with .txt extension', async () => {
|
||||
// ELF binary header disguised as .txt
|
||||
const elfHeader = Buffer.from([0x7F, 0x45, 0x4C, 0x46]); // \x7FELF
|
||||
const elfHeader = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // \x7FELF
|
||||
const buffer = Buffer.concat([elfHeader, Buffer.alloc(100)]);
|
||||
const result = await validateMimeType(buffer, 'notes.txt', 'text/plain', buffer.length);
|
||||
// file-type might detect this as application/x-elf or similar
|
||||
|
|
@ -166,14 +264,24 @@ describe('MIME Validation', () => {
|
|||
describe('Unrecognized extensions', () => {
|
||||
it('should reject files with unknown extensions', async () => {
|
||||
const buffer = Buffer.from('some data');
|
||||
const result = await validateMimeType(buffer, 'file.xyz', 'application/octet-stream', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'file.xyz',
|
||||
'application/octet-stream',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('not recognized');
|
||||
});
|
||||
|
||||
it('should reject files with no extension', async () => {
|
||||
const buffer = Buffer.from('some data');
|
||||
const result = await validateMimeType(buffer, 'noextension', 'application/octet-stream', buffer.length);
|
||||
const result = await validateMimeType(
|
||||
buffer,
|
||||
'noextension',
|
||||
'application/octet-stream',
|
||||
buffer.length
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -209,9 +317,15 @@ describe('MIME Validation', () => {
|
|||
|
||||
it('should have entries for office document types', () => {
|
||||
expect(ALLOWED_TYPES['application/pdf']).toBeDefined();
|
||||
expect(ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.wordprocessingml.document']).toBeDefined();
|
||||
expect(ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']).toBeDefined();
|
||||
expect(ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.presentationml.presentation']).toBeDefined();
|
||||
expect(
|
||||
ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
|
||||
).toBeDefined();
|
||||
expect(
|
||||
ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
).toBeDefined();
|
||||
expect(
|
||||
ALLOWED_TYPES['application/vnd.openxmlformats-officedocument.presentationml.presentation']
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have per-type size limits', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue