Skip to content

CWE-95 + frontend/template XSS precision - #73

Open
leenk7991 wants to merge 5 commits into
mainfrom
feat/cwe95-xss-sink-precision
Open

CWE-95 + frontend/template XSS precision#73
leenk7991 wants to merge 5 commits into
mainfrom
feat/cwe95-xss-sink-precision

Conversation

@leenk7991

@leenk7991 leenk7991 commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

Stop Sighthound from reporting DOM APIs as CWE-95 eval injection, and cut Django/HTMX XSS noise without dropping real DOM XSS.

Problem: CWE-95 findings were innerHTML/htmx/DOMPurify writes, rated Critical. HTML-only files failed combined mode (No taint flow rules found) so Django |safe never ran. vm.runInNewContext(userInput) was silent (taint-only sink; frontend sources are DOM, not generic params; search prefilter dropped regex-escaped vm.runIn* names). escapeHtml (textContent then read innerHTML) was CWE-79.

Root cause: CWE-95 sink list too broad (any HTML write ≈ eval). Combined mode required taint rules even when the language only has search rules. HTML language did not visit text nodes. No search-mode vm pattern that survived the call-name prefilter.

Changes

  • eval sinks: eval / Function / setTimeout(string literal) / setInterval(string literal) / vm.runIn* only — not HTML writes, not setTimeout(handler, n)
  • Search timers match a quote after ( (setTimeout('…')); identifier callbacks stay quiet. Taint still flags a timer when the first arg is a known source
  • Dummy vm.runInNewContext= (and This/Context) names so the search prefilter keeps vm.runIn* (escaped regex dots are not a substring of the call name)
  • XSS: taint + unsafe HTML sink; autoescape / DOMPurify / json_script / hx-swap allow
  • escapeHtml: textContent write then innerHTML read is a sanitizer, not CWE-79
  • Django: |safe / |mark_safe on request.GET/POST/COOKIES only
  • Combined mode skips taint when there are 0 taint rules (HTML/Django search still runs)
  • HTML language visits text nodes so |safe matches when .html auto-detects as html
  • Backend SSTI (ejs.render / handlebars / mustache) is no longer CWE-95 (eval-family only; CWE-94 follow-up)
  • build.rs reruns compile when rules/ change
  • Ignore /sighthound_release/ (local platform export; do not commit the binary)
  • fixtures: TN innerHTML helper, escapeHtml, setTimeout/setInterval callbacks and setTimeout(handler); TP eval / Function / string-literal timers / vm / |safe

Test plan

  • existing JS XSS TPs still fire
  • new TNs do not fire CWE-95 (escapeHtml, DOMPurify, template.innerHTML, htmx.trigger, callback timers)
  • escapeHtml is not CWE-79
  • innerHTML = location.hash remains CWE-79
  • string setTimeout(userInput) is CWE-95; setTimeout(function () {…}) / setTimeout(handler) is not
  • string setInterval(userInput) is CWE-95; setInterval(function () {…}) is not
  • eval / new Function / Function('return '+x) / eval(location.hash) / vm.runInNewContext(userInput) are CWE-95
  • Django |safe and | mark_safe on request data are CWE-79; autoescape / json_script / hx-swap are not
  • scanning the same template as language html still flags |safe
  • cargo test --test strictness_tests -- cwe95_xss_sink_precision

E2E (fixture Django + JS app, this branch vs main):

  • eval / Function reported as CWE-95 (were CWE-94 on main)
  • setTimeout(userInput) with a string argument reported as CWE-95 (missing on main)
  • innerHTML = location.hash still reported as CWE-79
  • escapeHtml / createElement / parse-only innerHTML helpers no longer reported as CWE-95
  • setTimeout(function () {…}) callback not CWE-95
  • Django |safe / |mark_safe on request data reported as CWE-79 (missing on main)

Checklist

  • make ci passes locally (the same command CI runs — see CONTRIBUTING.md)
  • Ran make bootstrap once so pre-commit/pre-push hooks are active
  • Added/updated tests for the change
  • Updated docs/rules where relevant

linear ticket: https://linear.app/corgea/issue/COR-1802/

leenk7991 and others added 3 commits August 19, 2026 15:56
Skip the taint pass when a pack has no taint rules, and match template text on auto-detected HTML so CLI/Fusion no longer drop search findings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Search-mode vm patterns and setInterval(string) TPs; treat textContent then innerHTML as a sanitizer. Identifier setTimeout callbacks stay quiet.

Co-authored-by: Cursor <cursoragent@cursor.com>
@leenk7991
leenk7991 marked this pull request as ready for review August 20, 2026 08:45
Comment thread build.rs Outdated
Comment thread build.rs Outdated
Comment thread src/scanner/utils.rs
@leenk7991
leenk7991 requested a review from juangaitanv August 24, 2026 09:36
Comment on lines +158 to +184
patterns: Some([
"settimeout-eval-sink=",
"regex:setTimeout\\(\\s*['\"]",
"regex:setInterval\\(\\s*['\"]"
]),
finding_type: Some("Code Injection"),
severity: Some("Critical"),
confidence: Some("High"),
cwe_id: Some("cwe-95"),
description: Some("setTimeout/setInterval with a string argument evaluates attacker-controlled code"),
file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),
tags: Some(["code-injection", "frontend", "cwe-95"])
),
(
id: Some("js-code-injection-004"),
name: Some("Code injection via vm.runIn*"),
category: Some("code-injection"),
mode: "search",
// Dummy `=` names pass the call-name prefilter (escaped regex dots
// are not a substring of `vm.runInNewContext`).
patterns: Some([
"vm.runInNewContext=",
"vm.runInThisContext=",
"vm.runInContext=",
"regex:vm\\.runIn(?:New|This)?Context\\(\\s*[A-Za-z_$]",
"regex:vm\\.runIn(?:New|This)?Context\\(\\s*['\"]"
]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constant timer strings and trusted compiled scripts match as Critical CWE-95. could we require dynamic or untrusted input and leave identifiers to taint analysis?


sinks: Some([
// Direct code execution
// Eval injection only. Template engines / dynamic require are not CWE-95.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing these sinks leaves request-sourced server template injection uncovered. should we retain them under CWE-94 or include a replacement rule?

Comment thread src/language.rs
// names like `th:utext`, `th:replace`, or tag names like `textarea`
// resolve as the matchable "function" name for search rules.
match node.kind() {
"text" => django_template_name_from_text(get_node_text_slice(node, source)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Django filters in attribute values do not reach the safe-filter rule. could we extract template tokens from attribute values as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants