From 86d007d94c6d7fdc1192a8e63a8c4db7c8d30774 Mon Sep 17 00:00:00 2001 From: scops <2014109+scops@users.noreply.github.com> Date: Thu, 14 May 2026 23:00:28 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20hardened=20sanitize-html=20configura?= =?UTF-8?q?tion=20path=20against=20GHSA-rpr9-rxv7-x643=20by=20forcing=20di?= =?UTF-8?q?sallowed=20xmp=20content=20into=20nonTextTags=20unless=20xmp=20?= =?UTF-8?q?is=20explicitly=20allowed.=20That=20change=20is=20in=20lib/allo?= =?UTF-8?q?wlist.js.=20It=20covers=20both=20the=20built-in=20default=20pol?= =?UTF-8?q?icy=20and=20any=20custom=20policy=20loaded=20through=20ALLOWLIS?= =?UTF-8?q?T=5FFILE.=20Also=20added=20regressions=20in=20tests/allowlist.t?= =?UTF-8?q?est.js=20and=20tests/validation.test.js=20for=20the=20advisory?= =?UTF-8?q?=E2=80=99s=20xmp=20PoC=20path.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + lib/allowlist.js | 20 ++++++++++++++++++-- tests/allowlist.test.js | 17 +++++++++++++++++ tests/validation.test.js | 15 +++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ec8a94f..5ed4cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ docker-compose.override.yaml CLAUDE.md .codex/ AGENTS.md +.serena diff --git a/lib/allowlist.js b/lib/allowlist.js index 41e5f22..da36f15 100644 --- a/lib/allowlist.js +++ b/lib/allowlist.js @@ -1,5 +1,7 @@ const fs = require('node:fs'); +const RAW_TEXT_BYPASS_TAGS = Object.freeze(['xmp']); + const DEFAULT_ALLOWLIST = Object.freeze({ allowedTags: Object.freeze([ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'br', 'hr', @@ -16,8 +18,22 @@ const DEFAULT_ALLOWLIST = Object.freeze({ }), allowedSchemes: Object.freeze(['http', 'https', 'mailto']), disallowedTagsMode: 'discard', + nonTextTags: Object.freeze(['script', 'style', 'textarea', 'option', ...RAW_TEXT_BYPASS_TAGS]), }); +function hardenAllowlist(config) { + const allowedTags = Array.isArray(config.allowedTags) ? config.allowedTags : []; + if (allowedTags.includes('xmp')) return config; + + const nonTextTags = new Set(Array.isArray(config.nonTextTags) ? config.nonTextTags : []); + for (const tag of RAW_TEXT_BYPASS_TAGS) nonTextTags.add(tag); + + return { + ...config, + nonTextTags: [...nonTextTags], + }; +} + function loadAllowlist({ path = process.env.ALLOWLIST_FILE } = {}) { if (!path) return DEFAULT_ALLOWLIST; @@ -53,7 +69,7 @@ function loadAllowlist({ path = process.env.ALLOWLIST_FILE } = {}) { throw new Error('ALLOWLIST_FILE: "allowedSchemes" must be an array'); } - return parsed; + return hardenAllowlist(parsed); } -module.exports = { DEFAULT_ALLOWLIST, loadAllowlist }; +module.exports = { DEFAULT_ALLOWLIST, loadAllowlist, hardenAllowlist }; diff --git a/tests/allowlist.test.js b/tests/allowlist.test.js index 41e32c1..714774b 100644 --- a/tests/allowlist.test.js +++ b/tests/allowlist.test.js @@ -20,6 +20,7 @@ describe("loadAllowlist", () => { expect(loadAllowlist({ path: undefined })).toBe(DEFAULT_ALLOWLIST); expect(DEFAULT_ALLOWLIST.allowedTags).toContain("p"); expect(DEFAULT_ALLOWLIST.allowedTags).not.toContain("script"); + expect(DEFAULT_ALLOWLIST.nonTextTags).toContain("xmp"); }); it("reads a JSON file when a path is provided", () => { @@ -34,6 +35,22 @@ describe("loadAllowlist", () => { const loaded = loadAllowlist({ path: file }); expect(loaded.allowedTags).toEqual(["p", "em"]); expect(loaded.disallowedTagsMode).toBe("escape"); + expect(loaded.nonTextTags).toContain("xmp"); + } finally { + removeFixture(file); + } + }); + + it("does not force xmp into nonTextTags when xmp is explicitly allowed", () => { + const file = writeFixture("xmp-allowed", { + allowedTags: ["xmp"], + allowedAttributes: {}, + disallowedTagsMode: "discard", + }); + + try { + const loaded = loadAllowlist({ path: file }); + expect(loaded.nonTextTags).toBeUndefined(); } finally { removeFixture(file); } diff --git a/tests/validation.test.js b/tests/validation.test.js index 081f038..2ed3bcc 100644 --- a/tests/validation.test.js +++ b/tests/validation.test.js @@ -163,6 +163,21 @@ describe("Markdown Validator API", () => { }); }); + describe("sanitize-html hardening", () => { + it("drops xmp raw-text payloads instead of re-emitting active HTML", async () => { + const response = await request(app) + .post("/validate") + .send({ markdown: "