From ec534e6c4ac76241e153ca082251416cef741772 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 19 Jan 2026 19:14:33 +1100 Subject: [PATCH 1/2] Fix XSS vulnerability with comprehensive defense-in-depth + tests - Add sanitize-html for server-side input sanitization - Block javascript:/data: URIs and escape all reference data - Add DOMPurify 3.1.7 for robust client-side HTML sanitization - Add Helmet security headers with strict CSP - Replace deprecated url.parse() with native URL API - Add security test suite covering attack vectors --- docs/index.html | 1 + docs/js/search.js | 72 +++++++++--- index.js | 27 ++++- lib/bibref.js | 31 +++++- package-lock.json | 199 +++++++++++++++++++++++++++++++++- package.json | 4 +- test/security.js | 271 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 580 insertions(+), 25 deletions(-) create mode 100644 test/security.js diff --git a/docs/index.html b/docs/index.html index 9584583a9..6c8002506 100644 --- a/docs/index.html +++ b/docs/index.html @@ -152,6 +152,7 @@

+ '; + var sanitized = testSanitizeInput(malicious); + assert.equal("", sanitized, "Script tags should be completely removed"); + }); + + test("strips img onerror handlers", function () { + var malicious = ""; + var sanitized = testSanitizeInput(malicious); + assert.equal("", sanitized, "Img tags with onerror should be removed"); + }); + + test("strips all HTML tags and attributes", function () { + var malicious = '
Click me
'; + var sanitized = testSanitizeInput(malicious); + assert.equal( + "Click me", + sanitized, + "HTML tags removed but text content preserved", + ); + }); + + test("handles empty and null inputs safely", function () { + assert.equal("", testSanitizeInput("")); + assert.equal("", testSanitizeInput(null)); + assert.equal("", testSanitizeInput(undefined)); + }); + }); + + suite("Client-side URL safety", function () { + test("blocks javascript: URLs", function () { + var malicious = "javascript:alert(1)"; + var safe = safeUrl(malicious); + assert.equal("", safe, "javascript: URLs should be blocked"); + }); + + test("blocks data: URLs", function () { + var malicious = "data:text/html,"; + var safe = safeUrl(malicious); + assert.equal("", safe, "data: URLs should be blocked"); + }); + + test("allows legitimate HTTP URLs", function () { + var legitimate = "https://www.w3.org/TR/html/"; + var safe = safeUrl(legitimate); + assert.equal( + "https://www.w3.org/TR/html/", + safe, + "Legitimate URLs should be allowed", + ); + }); + + test("handles case-insensitive protocol detection", function () { + assert.equal( + "", + safeUrl("JAVASCRIPT:alert(1)"), + "Uppercase javascript: should be blocked", + ); + assert.equal( + "", + safeUrl("JavaScript:alert(1)"), + "Mixed case javascript: should be blocked", + ); + assert.equal( + "", + safeUrl("DATA:text/html,'; + testEndpoint("search-refs", { q: scriptPayload }, function (err, result) { + if (err) return done(err); + assert.deepEqual( + result, + { message: "Missing q parameter" }, + "Script tags should be sanitized to empty, treated as missing parameter", + ); + done(); + }); + }); + + test("/reverse-lookup blocks XSS in urls parameter", function (done) { + var xssPayload = ""; + testEndpoint( + "reverse-lookup", + { urls: xssPayload }, + function (err, result) { + if (err) return done(err); + assert.deepEqual( + result, + {}, + "Malicious URL should return empty result", + ); + done(); + }, + ); + }); + + test("/reverse-lookup blocks javascript: URLs", function (done) { + var jsPayload = "javascript:alert(1)"; + testEndpoint( + "reverse-lookup", + { urls: jsPayload }, + function (err, result) { + if (err) return done(err); + assert.deepEqual( + result, + {}, + "JavaScript URLs should return empty result", + ); + done(); + }, + ); + }); + + test("/search-refs preserves legitimate queries", function (done) { + testEndpoint("search-refs", { q: "html" }, function (err, result) { + if (err) return done(err); + assert.ok( + typeof result === "object", + "Legitimate query should return object", + ); + assert.ok( + Object.keys(result).length > 0, + "Legitimate query should return results", + ); + done(); + }); + }); + + test("/reverse-lookup preserves legitimate URLs", function (done) { + testEndpoint( + "reverse-lookup", + { urls: "https://www.w3.org/TR/html/" }, + function (err, result) { + if (err) return done(err); + assert.ok( + typeof result === "object", + "Legitimate URL should return object", + ); + // May be empty if URL not in database, but should not error + done(); + }, + ); + }); + }); + + suite("Integration security tests", function () { + test("XSS payload completely neutralized through full pipeline", function () { + var xssPayload = ""; + var serverSanitized = testSanitizeInput(xssPayload); + assert.equal( + "", + serverSanitized, + "Server should strip malicious HTML completely", + ); + }); + + test("Legitimate content preserved through pipeline", function () { + var legitimate = "HTML specification"; + var serverSanitized = testSanitizeInput(legitimate); + assert.equal( + "HTML specification", + serverSanitized, + "Legitimate text preserved", + ); + }); + + test("Mixed content handled safely", function () { + var mixed = "Normal text more text"; + var serverSanitized = testSanitizeInput(mixed); + assert.equal("Normal text more text", serverSanitized); + }); + }); + + suite("Edge case security tests", function () { + test("handles nested and encoded XSS attempts", function () { + var nested = "<script>alert(1)</script>"; + var sanitized = testSanitizeInput(nested); + // sanitize-html keeps HTML entities as-is when no tags are allowed + // This is safe because <script> cannot execute as JavaScript + assert.equal( + "<script>alert(1)</script>", + sanitized, + "HTML entities kept safe and non-executable", + ); + }); + + test("handles URL with malicious fragments", function () { + var maliciousUrl = "https://example.com#"; + var safe = safeUrl(maliciousUrl); + // URL is allowed through since it has legitimate https protocol + // The fragment with script tags will be handled by DOMPurify during HTML sanitization + assert.ok( + safe.includes("example.com"), + "Legitimate domain should be preserved", + ); + }); + }); +}); From 395a58adbc1823c61b1210cd454f20443d697e1c Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Tue, 20 Jan 2026 12:49:54 +1100 Subject: [PATCH 2/2] use https --- lib/bibref.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bibref.js b/lib/bibref.js index 9be693187..a65df0eb2 100644 --- a/lib/bibref.js +++ b/lib/bibref.js @@ -120,10 +120,10 @@ function normalizeUrl(u) { return ''; } - // If no protocol, try adding http:// for parsing + // If no protocol, default to https:// for security var urlToParse = u; if (!/^https?:\/\//.test(u)) { - urlToParse = 'http://' + u; + urlToParse = 'https://' + u; } var urlObj = new URL(urlToParse);