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(
+ !/