Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions web/protocol/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const WEB_MAX_ATTACHMENTS = 8;
export const WEB_MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
export const WEB_MAX_ATTACHMENT_TOTAL_BYTES = 8 * 1024 * 1024;
const MIME_EXTENSIONS: Record<string, readonly string[]> = {
"text/plain": [".txt", ".log"], "text/markdown": [".md", ".markdown"],
"application/json": [".json"], "image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"], "image/webp": [".webp"],
};
export interface WebAttachmentInput { readonly name: string; readonly mime: string; readonly size: number; }
export function validateWebAttachments(attachments: readonly WebAttachmentInput[]) {
if (attachments.length > WEB_MAX_ATTACHMENTS) return { ok: false as const, error: "too many attachments" };
let total = 0;
for (const attachment of attachments) {
if (!/^(?!\.\.?(?:$|\.))[\w .()\[\]-]{1,120}$/u.test(attachment.name) || attachment.name.includes("..")) return { ok: false as const, error: "invalid attachment name" };
if (!MIME_EXTENSIONS[attachment.mime]) return { ok: false as const, error: "unsupported attachment type" };
if (!MIME_EXTENSIONS[attachment.mime].some((extension) => attachment.name.toLocaleLowerCase().endsWith(extension))) return { ok: false as const, error: "attachment extension does not match type" };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reject inherited object keys before invoking the extension list

MIME_EXTENSIONS is an ordinary object, so a MIME value of constructor or __proto__ passes the truthiness check on the preceding line. This line then throws TypeError: MIME_EXTENSIONS[attachment.mime].some is not a function, rather than returning { ok: false, error: "unsupported attachment type" }. I reproduced both with the exact-head module and validateWebAttachments([{ name: "x.txt", mime, size: 1 }]); the normal text/plain case succeeds.

Use an own-property whitelist lookup (or a Map/null-prototype record) and add regression cases for inherited keys so arbitrary unsupported MIME strings are rejected without throwing. This is the pure admission contract, not evidence of an active upload endpoint.

if (!Number.isSafeInteger(attachment.size) || attachment.size < 0 || attachment.size > WEB_MAX_ATTACHMENT_BYTES) return { ok: false as const, error: "attachment exceeds per-file limit" };
total += attachment.size;
if (total > WEB_MAX_ATTACHMENT_TOTAL_BYTES) return { ok: false as const, error: "attachments exceed total limit" };
}
return { ok: true as const };
}
Loading