Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ release/
# External benchmark corpus (separate/large repo, cloned locally)
/fusion-benchmarks/

# Local linux/mac export from `./build_all_platforms.sh` (binary + copied rules)
/sighthound_release/

# Generated benchmark scan artifacts (reproducible via bench/run_bench.py)
/bench/results/
/bench/*.json
Expand Down
24 changes: 24 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
fn main() {
emit_rerun("rules");
}

fn emit_rerun(root: &str) {
let mut pending = vec![std::path::PathBuf::from(root)];
while let Some(path) = pending.pop() {
println!("cargo:rerun-if-changed={}", path.display());
let entries = std::fs::read_dir(&path).unwrap_or_else(|error| {
panic!("failed to read directory '{}': {error}", path.display())
});
for entry in entries {
let entry = entry.unwrap_or_else(|error| {
panic!("failed to read directory entry in '{}': {error}", path.display())
});
let child = entry.path();
if child.is_dir() {
pending.push(child);
} else {
println!("cargo:rerun-if-changed={}", child.display());
}
}
}
}
58 changes: 45 additions & 13 deletions rules/backend_javascript/backend_security.ron
Original file line number Diff line number Diff line change
Expand Up @@ -474,25 +474,14 @@
]),

sinks: Some([
// Direct code execution
// Eval injection only. Template engines / dynamic require are not CWE-95.
Comment thread
leenk7991 marked this conversation as resolved.
"eval",
"Function",
"setTimeout",
"setInterval",

// VM module
"vm.runInThisContext",
"vm.runInNewContext",
"vm.runInContext",

// Dynamic requires
"require",
"import",

// Template engines (unsafe usage)
"ejs.render",
"handlebars.compile",
"mustache.render"
"vm.runInContext"
]),

sanitizers: Some([
Expand Down Expand Up @@ -525,6 +514,49 @@
)),

tags: Some(["code-injection", "cwe-94", "server-side", "backend"])
),

(
id: Some("js-server-template-injection-001"),
name: Some("Server-side template injection"),
category: Some("code-injection"),
mode: "taint",
sources: Some([
"req.query.*",
"req.params.*",
"req.body.*",
"request.query.*",
"request.params.*",
"request.body.*",
"ctx.query.*",
"ctx.params.*",
"req.headers.*"
]),
sinks: Some([
"ejs.render",
"handlebars.compile",
"mustache.render"
]),
sanitizers: Some([
"validateCode",
"sanitizeCode",
"isValidCode"
]),
finding_type: Some("Code Injection"),
severity: Some("Critical"),
confidence: Some("High"),
cwe_id: Some("cwe-94"),
description: Some("User-controlled request data is passed to a template engine"),
file_types: Some((
extensions: Some([".js", ".jsx", ".ts", ".tsx"]),
exclude_patterns: Some([
"*.min.js", "*.min.jsx", "*.min.ts", "*.min.tsx",
"*/public/*", "*/static/*", "*/assets/*", "*/dist/*",
"*/build/*", "*/frontend/*", "*/client/*",
"*/node_modules/*", "*/lib/*", "*/vendor/*"
])
)),
tags: Some(["code-injection", "cwe-94", "ssti", "server-side", "backend"])
)
]
)
31 changes: 31 additions & 0 deletions rules/html/django.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
(
rules: [
// Autoescaped `{{ var }}` is not XSS. Flag `|safe`/`|mark_safe` only on
// request data inside one `{{ ... }}` (`[^}]*` cannot join adjacent
// expressions). Dummy `=` names pass the `|safe` / `|mark_safe` / `{{`
// call-name prefilter (`django-{{=` contains `{{` but is not template
// syntax). Spaced filters (`| safe`) resolve as `{{`.
(
id: Some("html-django-safe-request-xss"),
name: Some("Django |safe on request data"),
category: Some("xss"),
mode: "search",
patterns: Some([
"django-safe-filter-sink=",
"|safe=",
"|mark_safe=",
"django-{{=",
"regex:\\{\\{[^}]*request\\.(GET|POST|COOKIES)[^}]*\\|\\s*(mark_)?safe[^}]*\\}\\}"
]),
finding_type: Some("Cross-Site Scripting"),
severity: Some("High"),
confidence: Some("High"),
cwe_id: Some("cwe-79"),
description: Some("Request data passed through |safe / |mark_safe disables autoescape and is reflected XSS"),
file_types: Some((
extensions: Some([".html", ".htm"])
)),
tags: Some(["xss", "django", "cwe-79"])
)
]
)
5 changes: 5 additions & 0 deletions rules/html/xss.ron
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
// All real sinks require a template literal (backtick) sink target, i.e.
// dynamic/interpolated HTML, which avoids flagging assignments of static
// string constants.
//
// Not sinks: HTMX `hx-swap="innerHTML"` (swap strategy, not reflected XSS),
// Django/Jinja default `{{ title }}` autoescape, or `json_script` /
// `<script type="application/json">` payloads. Unescaped `|safe` of
// request data is covered in django.ron.
(
id: Some("html-inline-dom-xss-001"),
name: Some("DOM XSS via dynamic HTML sink in inline script"),
Expand Down
11 changes: 9 additions & 2 deletions rules/javascript/backend_code_injection.ron
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@
patterns: Some([
"regex:eval\\(\\s*[A-Za-z_$](?:key)?",
"regex:new Function\\(\\s*[A-Za-z_$](?:key)?",
// Dummy `=` names pass the call-name prefilter (escaped regex
// dots are not a substring of `vm.runInNewContext`).
"vm.runInNewContext=",
"vm.runInThisContext=",
"vm.runInContext=",
"regex:vm\\.runIn(?:New|This)?Context\\(\\s*[A-Za-z_$]",
"regex:vm\\.runIn(?:New|This)?Context\\(\\s*['\"]",
]),
finding_type: Some("Code Injection"),
severity: Some("Critical"),
confidence: Some("High"),
cwe_id: Some("cwe-94"),
cwe_id: Some("cwe-95"),
description: Some("User-controlled input passed to a dynamic code evaluation sink"),
file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),
tags: Some(["injection", "code", "backend", "cwe-94"])
tags: Some(["injection", "code", "backend", "cwe-94", "cwe-95"])
),
],
)
58 changes: 55 additions & 3 deletions rules/javascript/frontend_security.ron
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@
category: Some("code-injection"),
mode: "search",
patterns: Some([
"*Function(*user*)", "*Function(*input*)", "*Function(*data*)", "*Function(*+*)"
"*Function(*user*)", "*Function(*input*)", "*Function(*data*)",
"new Function(*+*)", " Function(*+*)",
// Word boundary so `Function('return '+x)` matches but `myFunction` does not.
"regex:\\bFunction\\(.*\\+"
]),
finding_type: Some("Code Injection"),
severity: Some("Critical"),
Expand All @@ -144,6 +147,55 @@
file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),
tags: Some(["code-injection", "frontend", "cwe-80", "cwe-95"])
),
(
id: Some("js-code-injection-003"),
name: Some("Code injection via setTimeout/setInterval string"),
category: Some("code-injection"),
mode: "search",
// Gate `=` forces full-call matching. Dummy names pass the call-name
// prefilter for qualified `window.setTimeout` / `window.setInterval`
// (`regex:setTimeout\\(` is not a substring of that callee). First arg
// must be a concatenated or interpolated string, not a constant
// (`setTimeout('tick')`) or identifier callback (`setTimeout(handler, n)`).
patterns: Some([
"settimeout-eval-sink=",
"window.setTimeout=",
"window.setInterval=",
"regex:setTimeout\\(\\s*['\"][^'\"]*['\"]\\s*\\+",
"regex:setTimeout\\(\\s*`[^`]*\\$\\{",
"regex:setInterval\\(\\s*['\"][^'\"]*['\"]\\s*\\+",
"regex:setInterval\\(\\s*`[^`]*\\$\\{"
]),
Comment thread
leenk7991 marked this conversation as resolved.
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*['\"]"
]),
Comment thread
leenk7991 marked this conversation as resolved.
finding_type: Some("Code Injection"),
severity: Some("Critical"),
confidence: Some("High"),
cwe_id: Some("cwe-95"),
description: Some("User-controlled input is evaluated as JavaScript via vm.runIn*"),
file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),
tags: Some(["code-injection", "frontend", "cwe-95"])
),
(
id: Some("js-postmessage-001"),
name: Some("postMessage with wildcard origin"),
Expand Down Expand Up @@ -299,10 +351,10 @@
finding_type: Some("Dynamic Code Loading"),
severity: Some("High"),
confidence: Some("Medium"),
cwe_id: Some("cwe-95"),
cwe_id: Some("cwe-94"),
description: Some("User-controlled module paths can lead to code injection"),
file_types: Some((extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),
tags: Some(["code-injection","dynamic-import","frontend","cwe-95"])
tags: Some(["code-injection","dynamic-import","frontend","cwe-94"])
),
(
id: Some("js-redos-001"),
Expand Down
68 changes: 19 additions & 49 deletions rules/javascript/frontend_taint_security.ron
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,7 @@
"*.onload",
"*.onerror",
"*.setAttribute",
// Code execution
"eval(",
"Function(",
"setTimeout(", // With string argument
"setInterval(", // With string argument

// Dynamic script loading
"import(",
// Script src assignment. eval/Function/setTimeout are CWE-95, not XSS.
"document.createElement(\"script\").src"
]),

Expand Down Expand Up @@ -1309,15 +1302,13 @@
"URLSearchParams(*innerHTML*",
"new URLSearchParams(*innerHTML*",

// DOM element content patterns to innerHTML
"*.textContent*innerHTML*",
"*.innerText*innerHTML*",
"*.text*innerHTML*",
"option.text*innerHTML*",
"element.textContent*innerHTML*",
"firstMember.querySelector(*).textContent*innerHTML*",
"document.querySelector(*).textContent*innerHTML*",
"select.previousElementSibling.textContent*innerHTML*",
// DOM element content assigned to innerHTML (write). Reading
// innerHTML after a textContent write is the escapeHtml helper.
"*.innerHTML*=*.textContent*",
"*.innerHTML*=*textContent*",
"*.textContent*innerHTML*=*",
"*.innerText*innerHTML*=*",
"*.text*innerHTML*=*",

// Template literal patterns with innerHTML
"`*${*searchParams.get(*}*`*innerHTML*",
Expand Down Expand Up @@ -1384,25 +1375,15 @@
]),

sinks: Some([
// Code evaluation
// Eval injection only. DOM/HTML writes are CWE-79.
"eval(",
"Function(",
"new Function(",
"setTimeout(",
"setInterval(",

// Dynamic imports
"import(",
"require(",

// Script creation and execution
"document.createElement(\"script\")",
"*.appendChild",
"*.insertBefore",

// WebAssembly
"WebAssembly.compile",
"WebAssembly.instantiate"
"vm.runInNewContext",
"vm.runInThisContext",
"vm.runInContext"
]),

sanitizers: Some([
Expand Down Expand Up @@ -1791,26 +1772,15 @@
]),

sinks: Some([
// Direct code execution
// Eval injection only. DOM/HTML writes are CWE-79.
"eval(",
"new Function(",

// Timed execution with string
"Function(",
"setTimeout(",
"setInterval(",

// Dynamic imports
"import(",
"require(",

// Script creation and execution
"document.createElement",
"*.appendChild",
"*.insertBefore",

// WebAssembly
"WebAssembly.compile",
"WebAssembly.instantiate"
"vm.runInNewContext",
"vm.runInThisContext",
"vm.runInContext"
]),

sanitizers: Some([
Expand Down Expand Up @@ -2020,14 +1990,14 @@
finding_type: Some("Dynamic Code Loading"),
severity: Some("High"),
confidence: Some("Medium"),
cwe_id: Some("cwe-95"),
cwe_id: Some("cwe-94"),

description: Some("User-controlled module paths can lead to arbitrary code execution"),

file_types: Some((
extensions: Some([".js", ".jsx", ".ts", ".tsx"]))),

tags: Some(["code-injection", "cwe-95", "dynamic-import"])
tags: Some(["code-injection", "cwe-94", "dynamic-import"])
),

// ==================== CWE-601: URL Manipulation ====================
Expand Down
Loading
Loading