From 86f03102bf5c8a91b2d728dd349d2b3e08fe31bc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 17:58:11 +0000 Subject: [PATCH 1/2] rules(js): add balanced innerHTML/outerHTML DOM XSS rule The only innerHTML coverage in search mode was js-dom-xss-001, which keys off identifier names (*user*, *input*, *data*, *param*) and therefore misses the common shapes of the bug: template-literal interpolation, markup concatenated with an expression, and plain assignment of a variable or call result. The new rule matches those three shapes with anchored regexes so a match stays on the assignment that starts the node (enclosing blocks are no longer reported in place of the sink line), while static literals, reads and comparisons stay quiet. Sanitized values are dropped by the existing XSS sanitization check. Confidence is Medium because search mode cannot see sanitization that happened on an earlier line. Co-authored-by: Ahmad Sadeddin --- rules/javascript/frontend_security.ron | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/rules/javascript/frontend_security.ron b/rules/javascript/frontend_security.ron index c01afcd..4117a0b 100644 --- a/rules/javascript/frontend_security.ron +++ b/rules/javascript/frontend_security.ron @@ -45,6 +45,30 @@ file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))), tags: Some(["xss", "dom", "frontend", "cwe-79"]) ), + ( + id: Some("js-dom-xss-innerhtml-001"), + name: Some("DOM XSS via dynamic innerHTML/outerHTML assignment"), + category: Some("xss"), + mode: "search", + // The leading `^[^=;{]*` keeps a match anchored to the assignment that starts the + // matched node, so enclosing blocks are not reported instead of the sink line. + patterns: Some([ + // Interpolated template literal: el.innerHTML = `${value}` + "regex:^[^=;{]*\\.(innerHTML|outerHTML)\\b[^=]*=[^=][\\s\\S]*\\$\\{", + // HTML markup concatenated with an expression: + // el.innerHTML = '' + value / el.innerHTML = value + '' + "regex:^[^=;{]*\\.(innerHTML|outerHTML)\\b[^=]*=[^=][\\s\\S]*(['\"`]\\s*\\+\\s*[A-Za-z_$]|[A-Za-z_$][\\w$]*\\s*\\+\\s*['\"`])", + // Non-literal value: el.innerHTML = value / render(value), el.innerHTML += value + "regex:^[^=;{]*\\.(innerHTML|outerHTML)\\b[^=]*=\\s*[A-Za-z_$]" + ]), + finding_type: Some("DOM XSS"), + severity: Some("High"), + confidence: Some("Medium"), + cwe_id: Some("cwe-79"), + description: Some("innerHTML/outerHTML is assigned a value the scanner cannot prove is static HTML, so attacker-controlled markup can execute; assign textContent or sanitize with DOMPurify instead"), + file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))), + tags: Some(["xss", "dom", "frontend", "cwe-79"]) + ), ( id: Some("js-react-dangerously-set-inner-html-001"), name: Some("React DOM XSS via dangerouslySetInnerHTML"), From f8f4c1478a7b9d5430b47c53cbbef55d074bd416 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 17:58:18 +0000 Subject: [PATCH 2/2] scanner: recognize escapeHTML/sanitizeHtml as HTML sanitizers The JS/TS sanitizer list accepted escapeHtml( but not the escapeHTML( casing the rule files themselves list, and omitted sanitizeHtml( (sanitize-html) entirely, so `el.innerHTML = escapeHTML(v)` was reported as XSS. Co-authored-by: Ahmad Sadeddin --- src/scanner/utils.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/scanner/utils.rs b/src/scanner/utils.rs index 922ebc8..2e1c4b7 100644 --- a/src/scanner/utils.rs +++ b/src/scanner/utils.rs @@ -606,8 +606,16 @@ impl AstUtils { } fn check_html_sanitization(code: &str) -> bool { - let html_sanitizers = - ["DOMPurify.sanitize(", "validator.escape(", "xss(", "escapeHtml(", "encodeHTML("]; + let html_sanitizers = [ + "DOMPurify.sanitize(", + "validator.escape(", + "xss(", + "escapeHtml(", + "escapeHTML(", + "encodeHTML(", + "sanitizeHtml(", + "sanitizeHTML(", + ]; html_sanitizers.iter().any(|pat| code.contains(pat)) }