From 8b6eed1e59386e22a01a601515d6fef0a2b6655d Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Thu, 30 Apr 2026 23:19:40 +0000 Subject: [PATCH] EM #241: sanitize location/meetingUrl/notes (regex stripper) + tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original task: Executive Meetings — test coverage + sanitization closeout (umbrella for #170, #186-189, #201, #202, #212, #214, #218, #235). What landed - Added `stripTagsToPlainText[OrNull]` in `artifacts/api-server/src/lib/sanitize.ts`. Implementation is a two-pass regex stripper (NOT sanitize-html + entity decode): Pass 1: drop dangerous tag bodies entirely (`` → "" (tag and content discarded) + * - `` → "" (tag AND inner JS body dropped) + * - `` / `` → "" (body dropped) + * - `bold hi` → "bold hi" (inline tag dropped, text kept) + * - `<script>…</script>` → unchanged (no entity decode) * - `https://x.test?a=1&b=2#frag` → unchanged * - `5 < 10 and 20 > 5` → unchanged * @@ -110,17 +135,19 @@ export function stripTagsToPlainText(input: unknown): string { if (input === null || input === undefined) return ""; const s = typeof input === "string" ? input : String(input); if (s.length === 0) return ""; - const stripped = sanitizeHtml(s, { allowedTags: [], allowedAttributes: {} }); - return stripped - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/'/g, "'") - .replace(///g, "/") - .replace(/`/g, "`") - .replace(/`/g, "`"); + // Pass 1: drop tags whose body is not human-readable text along with + // their content, so e.g. `` doesn't leak the + // literal string `alert(1)` into the DB. + let out = s.replace(DANGEROUS_TAG_BODY_RE, ""); + // Pass 2: strip remaining HTML constructs (comments, CDATA, DOCTYPE, + // processing instructions, and any leftover open/close tags). + out = out + .replace(HTML_COMMENT_RE, "") + .replace(HTML_CDATA_RE, "") + .replace(HTML_DOCTYPE_RE, "") + .replace(HTML_PROCINST_RE, "") + .replace(HTML_TAG_RE, ""); + return out; } /** diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index aa98a185..92c01be1 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -479,6 +479,71 @@ test("Sanitization: POST strips HTML from location, meetingUrl, and notes", asyn ); }); +// Regression guard: an earlier sanitizer used a "strip tags then decode +// HTML entities" approach, which let a payload like `<script>…` +// round-trip into the DB as a literal `Boardroom B", + notes: "visible note", + attendees: [], + }); + assert.equal(mixed.status, 201); + const mm = await mixed.json(); + created.meetingIds.push(mm.id); + assert.ok( + !/ { const create = await api(adminCookie, "POST", "/api/executive-meetings", { titleAr: "م",