160 lines
4.0 KiB
TypeScript
160 lines
4.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
|
|
inspectBrowserImage,
|
|
} from './image-safety.js';
|
|
|
|
describe('browser image safety inspection', () => {
|
|
it('accepts bounded PNG and JPEG dimensions', () => {
|
|
expect(inspectBrowserImage(png(640, 480))).toMatchObject({
|
|
format: 'png',
|
|
extension: 'png',
|
|
mediaType: 'image/png',
|
|
width: 640,
|
|
height: 480,
|
|
pixels: 307_200,
|
|
browserRenderable: true,
|
|
});
|
|
expect(inspectBrowserImage(jpeg(1_920, 1_080))).toMatchObject({
|
|
format: 'jpeg',
|
|
extension: 'jpg',
|
|
mediaType: 'image/jpeg',
|
|
width: 1_920,
|
|
height: 1_080,
|
|
pixels: 2_073_600,
|
|
browserRenderable: true,
|
|
});
|
|
});
|
|
|
|
it('rejects huge PNG and JPEG dimensions without losing their type', () => {
|
|
expect(inspectBrowserImage(png(0xffff_ffff, 1))).toMatchObject({
|
|
format: 'png',
|
|
mediaType: 'image/png',
|
|
width: 0xffff_ffff,
|
|
height: 1,
|
|
browserRenderable: false,
|
|
rejectionReason: 'dimension-limit',
|
|
});
|
|
expect(inspectBrowserImage(jpeg(0xffff, 0xffff))).toMatchObject({
|
|
format: 'jpeg',
|
|
mediaType: 'image/jpeg',
|
|
width: 0xffff,
|
|
height: 0xffff,
|
|
browserRenderable: false,
|
|
rejectionReason: 'dimension-limit',
|
|
});
|
|
});
|
|
|
|
it('enforces decoded pixel and compressed-byte limits independently', () => {
|
|
expect(
|
|
inspectBrowserImage(png(8_192, 8_193), {
|
|
...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
|
|
maxDimension: 10_000,
|
|
})
|
|
).toMatchObject({
|
|
pixels: 67_117_056,
|
|
browserRenderable: false,
|
|
rejectionReason: 'pixel-limit',
|
|
});
|
|
expect(
|
|
inspectBrowserImage(png(1, 1), {
|
|
...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
|
|
maxBytes: 32,
|
|
})
|
|
).toMatchObject({
|
|
width: 1,
|
|
height: 1,
|
|
browserRenderable: false,
|
|
rejectionReason: 'byte-limit',
|
|
});
|
|
});
|
|
|
|
it('rejects malformed PNG IHDR and JPEG marker streams', () => {
|
|
const malformedPng = png(16, 16);
|
|
malformedPng[11] = 12;
|
|
const malformedJpeg = jpeg(16, 16);
|
|
malformedJpeg[4] = 0;
|
|
malformedJpeg[5] = 1;
|
|
|
|
expect(inspectBrowserImage(malformedPng)).toMatchObject({
|
|
format: 'png',
|
|
browserRenderable: false,
|
|
rejectionReason: 'invalid-header',
|
|
});
|
|
expect(inspectBrowserImage(malformedJpeg)).toMatchObject({
|
|
format: 'jpeg',
|
|
browserRenderable: false,
|
|
rejectionReason: 'invalid-header',
|
|
});
|
|
expect(inspectBrowserImage(Uint8Array.of(0xff, 0xd8, 0xff))).toMatchObject({
|
|
format: 'jpeg',
|
|
browserRenderable: false,
|
|
rejectionReason: 'invalid-header',
|
|
});
|
|
});
|
|
|
|
it('caps a JPEG marker walk that never reaches a frame header', () => {
|
|
const bytes = new Uint8Array(2 + 5_000 * 2);
|
|
bytes.set([0xff, 0xd8], 0);
|
|
for (let offset = 2; offset < bytes.length; offset += 2) {
|
|
bytes[offset] = 0xff;
|
|
bytes[offset + 1] = 0xd0;
|
|
}
|
|
|
|
expect(inspectBrowserImage(bytes)).toMatchObject({
|
|
format: 'jpeg',
|
|
browserRenderable: false,
|
|
rejectionReason: 'invalid-header',
|
|
});
|
|
});
|
|
});
|
|
|
|
function png(width: number, height: number): Uint8Array {
|
|
const bytes = new Uint8Array(33);
|
|
bytes.set([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10], 0);
|
|
writeU32Be(bytes, 8, 13);
|
|
bytes.set([0x49, 0x48, 0x44, 0x52], 12);
|
|
writeU32Be(bytes, 16, width);
|
|
writeU32Be(bytes, 20, height);
|
|
bytes.set([8, 6, 0, 0, 0], 24);
|
|
return bytes;
|
|
}
|
|
|
|
function jpeg(width: number, height: number): Uint8Array {
|
|
return Uint8Array.of(
|
|
0xff,
|
|
0xd8,
|
|
0xff,
|
|
0xe0,
|
|
0x00,
|
|
0x02,
|
|
0xff,
|
|
0xc0,
|
|
0x00,
|
|
0x11,
|
|
0x08,
|
|
(height >>> 8) & 0xff,
|
|
height & 0xff,
|
|
(width >>> 8) & 0xff,
|
|
width & 0xff,
|
|
0x03,
|
|
0x01,
|
|
0x11,
|
|
0x00,
|
|
0x02,
|
|
0x11,
|
|
0x00,
|
|
0x03,
|
|
0x11,
|
|
0x00
|
|
);
|
|
}
|
|
|
|
function writeU32Be(bytes: Uint8Array, offset: number, value: number): void {
|
|
bytes[offset] = (value >>> 24) & 0xff;
|
|
bytes[offset + 1] = (value >>> 16) & 0xff;
|
|
bytes[offset + 2] = (value >>> 8) & 0xff;
|
|
bytes[offset + 3] = value & 0xff;
|
|
}
|