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
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
43 lines
1.4 KiB
TypeScript
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;
|
|
}
|