dorfteich/apps/api/src/files/magic-bytes.ts
Claude Sonnet 5 0fae699018
All checks were successful
CD / Build and push images (push) Successful in 2m2s
CI / Lint, typecheck, test (push) Successful in 1m43s
CI / Auth e2e pack (push) Successful in 1m48s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
Add file storage service and image upload API (#27)
Implements the FileStorage abstraction (uploads/<pondId>/<fileId> on the
mounted volume), the attachments model, and POST /ponds/:id/files, GET
/media/:fileId, DELETE /files/:id. Uploads are validated by sniffing
magic bytes rather than trusting the client's Content-Type/filename
(catches a renamed .html-as-.png), checked against the max_file_bytes
and storage_bytes quotas, and served with nosniff + immutable caching.

Closes #27
2026-07-08 10:35:03 +02:00

43 lines
1.4 KiB
TypeScript

import type { AttachmentMimeType } from '@dorfteich/shared';
/**
* Magic-byte signatures for the M2 image allowlist (ADR 0011). The
* client-declared MIME type and filename extension are never trusted —
* only the actual bytes decide, which is what catches a renamed
* `.html`-as-`.png` upload. SVG has no reliable magic-byte signature (it's
* XML) and is rejected in M2 regardless, per ADR 0011.
*/
const SIGNATURES: ReadonlyArray<{
mimeType: AttachmentMimeType;
matches: (buf: Buffer) => boolean;
}> = [
{
mimeType: 'image/png',
matches: (buf) =>
buf.length >= 8 &&
buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
},
{
mimeType: 'image/jpeg',
matches: (buf) => buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff,
},
{
mimeType: 'image/gif',
matches: (buf) =>
buf.length >= 6 &&
(buf.toString('ascii', 0, 6) === 'GIF87a' || buf.toString('ascii', 0, 6) === 'GIF89a'),
},
{
mimeType: 'image/webp',
matches: (buf) =>
buf.length >= 12 &&
buf.toString('ascii', 0, 4) === 'RIFF' &&
buf.toString('ascii', 8, 12) === 'WEBP',
},
];
/** Returns the sniffed image MIME type, or null when the bytes match none of the allowed signatures. */
export function sniffImageMimeType(buffer: Buffer): AttachmentMimeType | null {
return SIGNATURES.find((signature) => signature.matches(buffer))?.mimeType ?? null;
}