From 5bf88a1bd71e2dfd5875e74394e6d9c30b43891d Mon Sep 17 00:00:00 2001 From: seekskyworld Date: Fri, 4 Sep 2026 16:26:20 +0800 Subject: [PATCH] fix(web): validate attachment filename types Signed-off-by: seekskyworld --- web/protocol/attachments.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 web/protocol/attachments.ts diff --git a/web/protocol/attachments.ts b/web/protocol/attachments.ts new file mode 100644 index 00000000..391ee44f --- /dev/null +++ b/web/protocol/attachments.ts @@ -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 = { + "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" }; + 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 }; +}