Files
zzet--gortex/internal/astquery/rules_sast_javascript.go
T
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

260 lines
12 KiB
Go

package astquery
// JavaScript / TypeScript SAST rules. Patterns are written once and
// shipped to both grammars (the JS tree-sitter grammar covers the
// subset shared with TS; the TS grammar handles the type-suffixed
// constructs separately). Each rule's Pat carries both keys.
func init() {
registerJSEvalFunction()
registerJSDangerousSinks()
registerJSReactDangerouslySetHTML()
registerJSChildProcessExec()
registerJSRequireWithVariable()
registerJSCookieFlags()
registerJSCORSWildcard()
registerJSPostMessageWildcard()
registerJSWeakCryptoSubtle()
registerJSMathRandomForToken()
registerJSNoTLSReject()
registerJSJWTNone()
registerJSDocumentWrite()
registerJSTargetBlankNoopener()
registerJSLocationAssignUserInput()
}
// 1. eval / new Function — CWE-95
func registerJSEvalFunction() {
pat := `((call_expression function: (identifier) @fn) @match (#match? @fn "^(eval)$"))
((new_expression constructor: (identifier) @fn) @match (#eq? @fn "Function"))`
mustRegisterSAST(sastRule{
Name: "js-eval-use",
Description: "`eval(...)` / `new Function(...)` — executes an arbitrary string as JS. Any caller-controlled input is RCE.",
Severity: "error",
CWE: "CWE-95",
OWASP: "A03:2021-Injection",
Tags: []string{"injection", "code-injection"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 2. dangerous DOM sinks — CWE-79
func registerJSDangerousSinks() {
pat := `((assignment_expression
left: (member_expression property: (property_identifier) @prop)) @match
(#match? @prop "^(innerHTML|outerHTML|insertAdjacentHTML)$"))`
mustRegisterSAST(sastRule{
Name: "js-dom-innerhtml-assignment",
Description: "Direct assignment to `.innerHTML` / `.outerHTML` / `.insertAdjacentHTML` — bypasses the browser's auto-escaping. Use `textContent` or a sanitizer (DOMPurify).",
Severity: "warning",
CWE: "CWE-79",
OWASP: "A03:2021-Injection",
Tags: []string{"xss", "dom"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
func registerJSDocumentWrite() {
pat := `((call_expression
function: (member_expression object: (identifier) @obj property: (property_identifier) @prop)) @match
(#eq? @obj "document") (#match? @prop "^(write|writeln)$"))`
mustRegisterSAST(sastRule{
Name: "js-document-write",
Description: "`document.write(...)` / `document.writeln(...)` — writes raw HTML into the DOM, identical XSS risk as `innerHTML=`. Blocked by Trusted Types when enabled.",
Severity: "error",
CWE: "CWE-79",
Tags: []string{"xss", "dom", "deprecated"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 3. React dangerouslySetInnerHTML
func registerJSReactDangerouslySetHTML() {
mustRegisterSAST(sastRule{
Name: "js-react-dangerously-set-inner-html",
Description: "JSX `dangerouslySetInnerHTML={{__html: x}}` — React's deliberate XSS escape hatch. Any caller-controlled `x` is XSS. Use DOMPurify or render as text.",
Severity: "error",
CWE: "CWE-79",
OWASP: "A03:2021-Injection",
Tags: []string{"xss", "react"},
Pat: map[string]string{
"javascript": `((jsx_attribute (property_identifier) @attr) @match (#eq? @attr "dangerouslySetInnerHTML"))`,
// tsx (not typescript) — jsx_attribute only exists in the
// JSX-aware grammar. .tsx targets are retagged as "tsx"
// upstream so this query compiles cleanly.
"tsx": `((jsx_attribute (property_identifier) @attr) @match (#eq? @attr "dangerouslySetInnerHTML"))`,
},
})
}
// 4. child_process.exec / execSync / spawn shell:true
func registerJSChildProcessExec() {
pat := `((call_expression
function: (member_expression object: (identifier) @obj property: (property_identifier) @fn)) @match
(#match? @obj "^(child_process|cp)$") (#match? @fn "^(exec|execSync)$"))`
mustRegisterSAST(sastRule{
Name: "js-child-process-exec",
Description: "`child_process.exec(cmd)` / `execSync(cmd)` — runs `cmd` through `/bin/sh -c` (or `cmd.exe /s /c`). Any caller-controlled portion is command injection. Use `execFile` with an argv array.",
Severity: "error",
CWE: "CWE-78",
OWASP: "A03:2021-Injection",
Tags: []string{"command-injection", "node"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 5. require with a variable — module-load hijack
func registerJSRequireWithVariable() {
pat := `((call_expression function: (identifier) @fn
arguments: (arguments (identifier) @arg)) @match
(#eq? @fn "require"))`
mustRegisterSAST(sastRule{
Name: "js-require-with-variable",
Description: "`require(varName)` — module path comes from a variable. Caller-controlled values are arbitrary file load / require.cache poisoning. Prefer static `require('./fixed-path')`.",
Severity: "warning",
CWE: "CWE-915",
Tags: []string{"dynamic-import", "node"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 6. Cookie flags — secure=false / httpOnly=false
func registerJSCookieFlags() {
pat := `((object (pair key: (property_identifier) @key value: (false))) @match
(#match? @key "^(secure|httpOnly|httpolnly)$"))`
mustRegisterSAST(sastRule{
Name: "js-cookie-no-secure-or-httponly",
Description: "Cookie option `{ secure: false }` or `{ httpOnly: false }` — secure=false transmits the cookie over plaintext HTTP; httpOnly=false exposes it to `document.cookie`-reading XSS. Flip to true unless a JS reader is genuinely required.",
Severity: "warning",
CWE: "CWE-614",
Tags: []string{"cookies"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 7. CORS allow-all + credentials true
func registerJSCORSWildcard() {
pat := `((call_expression
function: (identifier) @fn
arguments: (arguments (object) @opts)) @match
(#match? @fn "^(cors|Cors)$"))`
mustRegisterSAST(sastRule{
Name: "js-cors-wildcard-with-credentials",
Description: "`cors({ origin: '*', credentials: true })` — invalid per the CORS spec, and many libraries fall back to reflecting the request `Origin` header. Use an explicit allow-list.",
Severity: "info",
CWE: "CWE-942",
Tags: []string{"cors", "audit-hook"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 8. postMessage with target="*"
func registerJSPostMessageWildcard() {
pat := `((call_expression
function: (member_expression property: (property_identifier) @fn)
arguments: (arguments . (_) (string) @target)) @match
(#eq? @fn "postMessage") (#match? @target "\"\\*\""))`
mustRegisterSAST(sastRule{
Name: "js-postmessage-wildcard-target",
Description: "`window.postMessage(data, '*')` — any iframe can receive the message. Use an explicit origin (`'https://example.com'`).",
Severity: "warning",
CWE: "CWE-346",
Tags: []string{"postmessage"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 9. crypto.createHash('md5'|'sha1')
func registerJSWeakCryptoSubtle() {
pat := `((call_expression
function: (member_expression object: (identifier) @obj property: (property_identifier) @fn)
arguments: (arguments (string) @algo)) @match
(#eq? @obj "crypto") (#eq? @fn "createHash") (#match? @algo "[\"'](md5|sha1)[\"']"))`
mustRegisterSAST(sastRule{
Name: "js-crypto-weak-hash",
Description: "`crypto.createHash('md5')` / `crypto.createHash('sha1')` — cryptographically broken hashes. Use `'sha256'` or `'sha3-256'`.",
Severity: "error",
CWE: "CWE-327",
Tags: []string{"crypto", "weak-hash"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 10. Math.random for tokens / IDs
func registerJSMathRandomForToken() {
pat := `((call_expression
function: (member_expression object: (identifier) @obj property: (property_identifier) @fn)) @match
(#eq? @obj "Math") (#eq? @fn "random"))`
mustRegisterSAST(sastRule{
Name: "js-math-random-for-token",
Description: "`Math.random()` — not cryptographically secure. Audit hook for any site that produces an ID / token / nonce. Use `crypto.randomUUID()` / `crypto.getRandomValues()`.",
Severity: "info",
CWE: "CWE-338",
Tags: []string{"crypto", "audit-hook"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 11. NODE_TLS_REJECT_UNAUTHORIZED=0
func registerJSNoTLSReject() {
pat := `((property_identifier) @match (#eq? @match "NODE_TLS_REJECT_UNAUTHORIZED"))
((pair key: (property_identifier) @key value: (false)) @match (#eq? @key "rejectUnauthorized"))`
mustRegisterSAST(sastRule{
Name: "js-no-tls-reject-unauthorized",
Description: "`process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'` / `rejectUnauthorized: false` — disables Node.js TLS verification globally. Anything beyond a dev script is shipping a backdoor.",
Severity: "error",
CWE: "CWE-295",
Tags: []string{"tls", "no-verify"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
func registerJSTargetBlankNoopener() {
pat := `((jsx_attribute (property_identifier) @attr (string) @val) @match
(#eq? @attr "target") (#match? @val "[\"']_blank[\"']"))`
mustRegisterSAST(sastRule{
Name: "js-target-blank-no-noopener",
Description: "`<a target=\"_blank\">` without `rel=\"noopener noreferrer\"` lets the opened page navigate the opener via `window.opener`. Add the rel attribute or use `rel=\"noreferrer\"`.",
Severity: "info",
CWE: "CWE-1022",
Tags: []string{"phishing"},
// tsx (not typescript) — jsx_attribute only exists in the
// JSX-aware grammar. .tsx targets are retagged as "tsx"
// upstream so this query compiles cleanly.
Pat: map[string]string{"javascript": pat, "tsx": pat},
})
}
func registerJSLocationAssignUserInput() {
pat := `((assignment_expression
left: (member_expression object: (identifier) @loc property: (property_identifier) @prop)) @match
(#eq? @loc "location") (#match? @prop "^(href|search|pathname)$"))
((assignment_expression
left: (member_expression
object: (member_expression object: (identifier) @win property: (property_identifier) @loc)
property: (property_identifier) @prop)) @match
(#eq? @win "window") (#eq? @loc "location") (#match? @prop "^(href|search|pathname)$"))`
mustRegisterSAST(sastRule{
Name: "js-location-href-assignment",
Description: "`location.href = x` / `window.location.href = x` — if `x` comes from URL params it can be an `javascript:` URL → XSS. Validate the scheme allow-list.",
Severity: "info",
CWE: "CWE-79",
Tags: []string{"xss", "open-redirect"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}
// 12. JWT alg=none / verify:false
func registerJSJWTNone() {
pat := `((object (pair key: (property_identifier) @key value: (string) @val)) @match
(#eq? @key "algorithm") (#match? @val "[\"']none[\"']"))`
mustRegisterSAST(sastRule{
Name: "js-jwt-none-alg",
Description: "`{ algorithm: 'none' }` — produces / accepts unsigned JWTs. Trivial impersonation; reject any non-`HS256` / `RS256` / `EdDSA` algorithm explicitly.",
Severity: "error",
CWE: "CWE-347",
Tags: []string{"jwt", "broken-auth"},
Pat: map[string]string{"javascript": pat, "typescript": pat},
})
}