From 49ff7f91b416b993721674b608df0102227e172a Mon Sep 17 00:00:00 2001
From: Yusuf Tomilola <52901501+yusuftomilola@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:06:57 +0100
Subject: [PATCH 1/6] fix(xss): add DOMPurify sanitize utility with strict
allowlist
---
frontend/lib/utils/sanitize.ts | 68 ++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
create mode 100644 frontend/lib/utils/sanitize.ts
diff --git a/frontend/lib/utils/sanitize.ts b/frontend/lib/utils/sanitize.ts
new file mode 100644
index 0000000..c0869c1
--- /dev/null
+++ b/frontend/lib/utils/sanitize.ts
@@ -0,0 +1,68 @@
+/**
+ * sanitize.ts
+ *
+ * Wraps DOMPurify with a strict allowlist.
+ * - No Hello') // → 'Hello'
+ * sanitize('Bold') // → 'Bold'
+ * sanitize('x') // → 'x'
+ */
+export function sanitize(input: string): string {
+ if (typeof window === "undefined") {
+ // Server-side: DOMPurify requires a DOM. Strip all tags as a safe fallback.
+ return input.replace(/<[^>]*>/g, "");
+ }
+
+ return DOMPurify.sanitize(input, {
+ ALLOWED_TAGS,
+ ALLOWED_ATTR,
+ // Block javascript: and data: URIs in any attribute value
+ ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i,
+ // Prevent DOM clobbering
+ SANITIZE_DOM: true,
+ });
+}
+
+/**
+ * Strip ALL HTML tags — use this when you only need plain text output.
+ *
+ * @example
+ * stripTags('Hello world') // → 'Hello world'
+ */
+export function stripTags(input: string): string {
+ if (typeof window === "undefined") {
+ return input.replace(/<[^>]*>/g, "");
+ }
+ return DOMPurify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] });
+}
\ No newline at end of file
From d1e4efdfb884694253b05a288c9d7163c416203e Mon Sep 17 00:00:00 2001
From: Yusuf Tomilola <52901501+yusuftomilola@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:07:00 +0100
Subject: [PATCH 2/6] test(xss): add unit tests for sanitize() and stripTags()
---
frontend/lib/utils/sanitize.test.ts | 63 +++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
create mode 100644 frontend/lib/utils/sanitize.test.ts
diff --git a/frontend/lib/utils/sanitize.test.ts b/frontend/lib/utils/sanitize.test.ts
new file mode 100644
index 0000000..893ef0f
--- /dev/null
+++ b/frontend/lib/utils/sanitize.test.ts
@@ -0,0 +1,63 @@
+/**
+ * sanitize.test.ts
+ *
+ * Unit tests for the sanitize() and stripTags() utilities.
+ * Tests run in jsdom (Next.js / Jest default) which provides window + DOMPurify DOM.
+ */
+
+import { sanitize, stripTags } from "./sanitize";
+
+describe("sanitize()", () => {
+ it("passes plain text through unchanged", () => {
+ expect(sanitize("Hello, world!")).toBe("Hello, world!");
+ });
+
+ it("strips Safe text");
+ expect(result).not.toContain("">');
+ expect(result).not.toContain("data:");
+ });
+
+ it("allows safe inline formatting tags", () => {
+ const input = "Bold and italic and
";
+ const result = sanitize(input);
+ expect(result).toContain("Bold");
+ expect(result).toContain("italic");
+ });
+
+ it("handles empty string without throwing", () => {
+ expect(sanitize("")).toBe("");
+ });
+
+ it("handles deeply nested XSS attempt", () => {
+ const result = sanitize('
text