circle-ir 3.129.0 → 3.132.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +4 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +14 -7
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +124 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1031 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +12 -4
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +722 -14
- package/dist/core/circle-ir-core.cjs +4 -2
- package/dist/core/circle-ir-core.js +4 -2
- package/package.json +1 -1
|
@@ -11732,8 +11732,10 @@ var DEFAULT_SINKS = [
|
|
|
11732
11732
|
{ method: "newTransformer", class: "TransformerFactory", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
|
|
11733
11733
|
{ method: "transform", class: "Transformer", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
|
|
11734
11734
|
// SSRF - Server-Side Request Forgery (CWE-918) and Open Redirect (CWE-601)
|
|
11735
|
-
|
|
11736
|
-
|
|
11735
|
+
// Sprint 82 (#189): HttpServletResponse.sendRedirect is CWE-601 / open_redirect,
|
|
11736
|
+
// not ssrf. Re-typing so manifest sink_type='open_redirect' matches.
|
|
11737
|
+
{ method: "sendRedirect", class: "HttpServletResponse", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
|
|
11738
|
+
{ method: "sendRedirect", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
|
|
11737
11739
|
{ method: "openConnection", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
11738
11740
|
{ method: "openStream", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
11739
11741
|
// NOTE: URL/URI constructors removed — constructing a URL object doesn't make a network
|
|
@@ -14611,12 +14613,19 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14611
14613
|
// splitting (CWE-113) — Sprint 6, issue #86.
|
|
14612
14614
|
// mass_assignment added to http_body / http_param: Object.assign(user, req.body),
|
|
14613
14615
|
// User(**request.form) — CWE-915.
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14616
|
+
// open_redirect added to http_param/http_query/http_header/http_cookie/http_body/http_path
|
|
14617
|
+
// Sprint 82 (#189): a user-controlled value reaching res.sendRedirect /
|
|
14618
|
+
// res.redirect / Location header / append_header(("Location", x)) /
|
|
14619
|
+
// Header().Set("Location", x) IS open_redirect (CWE-601). The reach map
|
|
14620
|
+
// previously omitted open_redirect so the inline-colocation flow detector
|
|
14621
|
+
// silently skipped all `http_* → open_redirect` co-located flows
|
|
14622
|
+
// (java sendRedirect, JS res.redirect, Rust append_header tuple, etc.).
|
|
14623
|
+
http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
|
|
14624
|
+
http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
|
|
14625
|
+
http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
|
|
14626
|
+
http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
|
|
14627
|
+
http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect"],
|
|
14628
|
+
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
|
|
14620
14629
|
// ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
|
|
14621
14630
|
// take a URL on stdin or as a positional CLI arg (`curl "$1"`,
|
|
14622
14631
|
// `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
|
|
@@ -14630,8 +14639,8 @@ function canSourceReachSink(sourceType, sinkType) {
|
|
|
14630
14639
|
network_input: ["sql_injection", "command_injection", "xss", "ssrf"],
|
|
14631
14640
|
config_param: ["sql_injection", "command_injection", "path_traversal", "xss", "ssrf"],
|
|
14632
14641
|
// Servlet init params
|
|
14633
|
-
interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
|
|
14634
|
-
// Cross-method taint
|
|
14642
|
+
interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
|
|
14643
|
+
// Cross-method taint; Sprint 82 (#189) — open_redirect added
|
|
14635
14644
|
plugin_param: ["sql_injection", "command_injection", "path_traversal", "xss", "code_injection"]
|
|
14636
14645
|
// Plugin/config parameters
|
|
14637
14646
|
};
|
|
@@ -23654,6 +23663,15 @@ var LanguageSourcesPass = class {
|
|
|
23654
23663
|
for (const finding of goMisconfigFindings) {
|
|
23655
23664
|
ctx.addFinding(finding);
|
|
23656
23665
|
}
|
|
23666
|
+
for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
|
|
23667
|
+
ctx.addFinding(finding);
|
|
23668
|
+
}
|
|
23669
|
+
for (const finding of findGoLocationHeaderOpenRedirectFindings(
|
|
23670
|
+
code,
|
|
23671
|
+
graph.ir.meta.file
|
|
23672
|
+
)) {
|
|
23673
|
+
ctx.addFinding(finding);
|
|
23674
|
+
}
|
|
23657
23675
|
}
|
|
23658
23676
|
if (language === "python") {
|
|
23659
23677
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -23666,6 +23684,18 @@ var LanguageSourcesPass = class {
|
|
|
23666
23684
|
for (const finding of pyMisconfigFindings) {
|
|
23667
23685
|
ctx.addFinding(finding);
|
|
23668
23686
|
}
|
|
23687
|
+
for (const finding of findPythonFlaskStringConcatXssFindings(code, graph.ir.meta.file)) {
|
|
23688
|
+
ctx.addFinding(finding);
|
|
23689
|
+
}
|
|
23690
|
+
for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
|
|
23691
|
+
ctx.addFinding(finding);
|
|
23692
|
+
}
|
|
23693
|
+
for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(
|
|
23694
|
+
code,
|
|
23695
|
+
graph.ir.meta.file
|
|
23696
|
+
)) {
|
|
23697
|
+
ctx.addFinding(finding);
|
|
23698
|
+
}
|
|
23669
23699
|
}
|
|
23670
23700
|
if (language === "rust") {
|
|
23671
23701
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -23687,6 +23717,12 @@ var LanguageSourcesPass = class {
|
|
|
23687
23717
|
for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
|
|
23688
23718
|
ctx.addFinding(finding);
|
|
23689
23719
|
}
|
|
23720
|
+
for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(
|
|
23721
|
+
code,
|
|
23722
|
+
graph.ir.meta.file
|
|
23723
|
+
)) {
|
|
23724
|
+
ctx.addFinding(finding);
|
|
23725
|
+
}
|
|
23690
23726
|
}
|
|
23691
23727
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
23692
23728
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -23694,9 +23730,23 @@ var LanguageSourcesPass = class {
|
|
|
23694
23730
|
additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
|
|
23695
23731
|
additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
|
|
23696
23732
|
additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
|
|
23733
|
+
additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
|
|
23734
|
+
additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
|
|
23697
23735
|
for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
|
|
23698
23736
|
ctx.addFinding(finding);
|
|
23699
23737
|
}
|
|
23738
|
+
for (const finding of findJsVueVHtmlXssFindings(code, graph.ir.meta.file)) {
|
|
23739
|
+
ctx.addFinding(finding);
|
|
23740
|
+
}
|
|
23741
|
+
for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
|
|
23742
|
+
ctx.addFinding(finding);
|
|
23743
|
+
}
|
|
23744
|
+
for (const finding of findJsDomOpenRedirectFindings(
|
|
23745
|
+
code,
|
|
23746
|
+
graph.ir.meta.file
|
|
23747
|
+
)) {
|
|
23748
|
+
ctx.addFinding(finding);
|
|
23749
|
+
}
|
|
23700
23750
|
}
|
|
23701
23751
|
if (language === "java") {
|
|
23702
23752
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -23708,6 +23758,9 @@ var LanguageSourcesPass = class {
|
|
|
23708
23758
|
for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
|
|
23709
23759
|
ctx.addFinding(finding);
|
|
23710
23760
|
}
|
|
23761
|
+
for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
|
|
23762
|
+
ctx.addFinding(finding);
|
|
23763
|
+
}
|
|
23711
23764
|
}
|
|
23712
23765
|
if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
|
|
23713
23766
|
const exfilFindings = findExternalSecretExfiltrationFindings(
|
|
@@ -25454,6 +25507,55 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
|
|
|
25454
25507
|
}
|
|
25455
25508
|
return sanitizers;
|
|
25456
25509
|
}
|
|
25510
|
+
function findJsArgvFormExecSanitizers(code) {
|
|
25511
|
+
const sanitizers = [];
|
|
25512
|
+
const lines = code.split("\n");
|
|
25513
|
+
const argvExecRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`)\s*,\s*\[/;
|
|
25514
|
+
const shellArgvRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*['"`](?:[\w./-]*\/)?(?:sh|bash|zsh|ksh|dash|cmd(?:\.exe)?|powershell|pwsh)['"`]\s*,\s*\[\s*['"`]-c['"`]/;
|
|
25515
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25516
|
+
const text = lines[i2];
|
|
25517
|
+
if (!argvExecRe.test(text)) continue;
|
|
25518
|
+
if (shellArgvRe.test(text)) continue;
|
|
25519
|
+
sanitizers.push({
|
|
25520
|
+
type: "js_argv_form_exec",
|
|
25521
|
+
method: "execFile",
|
|
25522
|
+
line: i2 + 1,
|
|
25523
|
+
sanitizes: ["command_injection", "external_taint_escape"]
|
|
25524
|
+
});
|
|
25525
|
+
}
|
|
25526
|
+
return sanitizers;
|
|
25527
|
+
}
|
|
25528
|
+
function findJsParameterizedSqlSanitizers(code) {
|
|
25529
|
+
const sanitizers = [];
|
|
25530
|
+
const lines = code.split("\n");
|
|
25531
|
+
const singleRe = /\.\s*(?:query|execute)\s*\(\s*'([^'\\]*(?:\\.[^'\\]*)*)'\s*,\s*\[/;
|
|
25532
|
+
const doubleRe = /\.\s*(?:query|execute)\s*\(\s*"([^"\\]*(?:\\.[^"\\]*)*)"\s*,\s*\[/;
|
|
25533
|
+
const tickRe = /\.\s*(?:query|execute)\s*\(\s*`([^`]*)`\s*,\s*\[/;
|
|
25534
|
+
const placeholderRe = /(?:\$\d+|\?)/;
|
|
25535
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25536
|
+
const text = lines[i2];
|
|
25537
|
+
let sql = null;
|
|
25538
|
+
const s = singleRe.exec(text);
|
|
25539
|
+
if (s) sql = s[1];
|
|
25540
|
+
if (sql == null) {
|
|
25541
|
+
const d = doubleRe.exec(text);
|
|
25542
|
+
if (d) sql = d[1];
|
|
25543
|
+
}
|
|
25544
|
+
if (sql == null) {
|
|
25545
|
+
const t = tickRe.exec(text);
|
|
25546
|
+
if (t && !/\$\{/.test(t[1])) sql = t[1];
|
|
25547
|
+
}
|
|
25548
|
+
if (sql == null) continue;
|
|
25549
|
+
if (!placeholderRe.test(sql)) continue;
|
|
25550
|
+
sanitizers.push({
|
|
25551
|
+
type: "js_parameterized_sql",
|
|
25552
|
+
method: "query",
|
|
25553
|
+
line: i2 + 1,
|
|
25554
|
+
sanitizes: ["sql_injection", "external_taint_escape"]
|
|
25555
|
+
});
|
|
25556
|
+
}
|
|
25557
|
+
return sanitizers;
|
|
25558
|
+
}
|
|
25457
25559
|
function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
25458
25560
|
const sanitizers = [];
|
|
25459
25561
|
const lines = code.split("\n");
|
|
@@ -26212,6 +26314,612 @@ function findJsPatternFindings(code, file) {
|
|
|
26212
26314
|
}
|
|
26213
26315
|
return out2;
|
|
26214
26316
|
}
|
|
26317
|
+
function findGoXssFindings(code, file) {
|
|
26318
|
+
const out2 = [];
|
|
26319
|
+
const lines = code.split("\n");
|
|
26320
|
+
const rwNames = /* @__PURE__ */ new Set();
|
|
26321
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
26322
|
+
for (const line of lines) {
|
|
26323
|
+
let m;
|
|
26324
|
+
sigRe.lastIndex = 0;
|
|
26325
|
+
while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
|
|
26326
|
+
}
|
|
26327
|
+
if (rwNames.size === 0) return out2;
|
|
26328
|
+
const escaperRe = /\b(?:html|template)\.(?:EscapeString|HTMLEscapeString|HTMLEscaper|JSEscapeString|URLQueryEscaper)\s*\(/;
|
|
26329
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26330
|
+
const raw = lines[i2];
|
|
26331
|
+
const trimmed = raw.trim();
|
|
26332
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26333
|
+
const callRe = /\bfmt\.Fprint(?:f|ln)?\s*\(\s*([A-Za-z_]\w*)\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
26334
|
+
const m = trimmed.match(callRe);
|
|
26335
|
+
if (!m) continue;
|
|
26336
|
+
if (!rwNames.has(m[1])) continue;
|
|
26337
|
+
const tail = m[2];
|
|
26338
|
+
if (escaperRe.test(tail)) continue;
|
|
26339
|
+
if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, ""))) continue;
|
|
26340
|
+
out2.push({
|
|
26341
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
26342
|
+
pass: "language-sources",
|
|
26343
|
+
category: "security",
|
|
26344
|
+
rule_id: "xss",
|
|
26345
|
+
cwe: "CWE-79",
|
|
26346
|
+
severity: "high",
|
|
26347
|
+
level: "error",
|
|
26348
|
+
message: "Reflected XSS: fmt.Fprint writes data to http.ResponseWriter without HTML escaping. Wrap user-controlled args with html.EscapeString / template.HTMLEscapeString.",
|
|
26349
|
+
file,
|
|
26350
|
+
line: i2 + 1,
|
|
26351
|
+
snippet: trimmed.substring(0, 100)
|
|
26352
|
+
});
|
|
26353
|
+
}
|
|
26354
|
+
return out2;
|
|
26355
|
+
}
|
|
26356
|
+
function findJavaResponseWriterXssFindings(code, file) {
|
|
26357
|
+
const out2 = [];
|
|
26358
|
+
const lines = code.split("\n");
|
|
26359
|
+
const respNames = /* @__PURE__ */ new Set();
|
|
26360
|
+
const sigRe = /\bHttpServletResponse\s+([A-Za-z_]\w*)\b/g;
|
|
26361
|
+
for (const line of lines) {
|
|
26362
|
+
let m;
|
|
26363
|
+
sigRe.lastIndex = 0;
|
|
26364
|
+
while ((m = sigRe.exec(line)) !== null) respNames.add(m[1]);
|
|
26365
|
+
}
|
|
26366
|
+
if (respNames.size === 0) return out2;
|
|
26367
|
+
const safeWrapRe = /\b(?:Encode\.(?:forHtml|forHtmlAttribute|forHtmlContent|forJavaScript)|StringEscapeUtils\.escape(?:Html3|Html4|EcmaScript|Xml)|HtmlUtils\.htmlEscape(?:Decimal|Hex)?|Escaper\.escapeHtml|HtmlEscapers\.(?:escapeHtml|htmlEscaper)|Encoder\.encodeForHTML(?:Attribute)?|Jsoup\.clean)\s*\(/;
|
|
26368
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26369
|
+
const raw = lines[i2];
|
|
26370
|
+
const trimmed = raw.trim();
|
|
26371
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
26372
|
+
const chainRe = /\b([A-Za-z_]\w*)\s*\.\s*getWriter\s*\(\s*\)\s*\.\s*(print|println|write|printf|format|append)\s*\(([\s\S]*)\)\s*;?\s*(?:\/\/.*)?$/;
|
|
26373
|
+
const m = trimmed.match(chainRe);
|
|
26374
|
+
if (!m) continue;
|
|
26375
|
+
const recv = m[1];
|
|
26376
|
+
if (!respNames.has(recv)) continue;
|
|
26377
|
+
const args2 = m[3];
|
|
26378
|
+
if (safeWrapRe.test(args2)) continue;
|
|
26379
|
+
const argsTrim = args2.trim();
|
|
26380
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(argsTrim)) continue;
|
|
26381
|
+
if (!/[A-Za-z_]\w*/.test(argsTrim)) continue;
|
|
26382
|
+
out2.push({
|
|
26383
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
26384
|
+
pass: "language-sources",
|
|
26385
|
+
category: "security",
|
|
26386
|
+
rule_id: "xss",
|
|
26387
|
+
cwe: "CWE-79",
|
|
26388
|
+
severity: "high",
|
|
26389
|
+
level: "error",
|
|
26390
|
+
message: "Reflected XSS: HttpServletResponse.getWriter()." + m[2] + "(...) writes data to the response without HTML escaping. Wrap user-controlled values with OWASP Encode.forHtml / StringEscapeUtils.escapeHtml4 / Jsoup.clean.",
|
|
26391
|
+
file,
|
|
26392
|
+
line: i2 + 1,
|
|
26393
|
+
snippet: trimmed.substring(0, 100)
|
|
26394
|
+
});
|
|
26395
|
+
}
|
|
26396
|
+
return out2;
|
|
26397
|
+
}
|
|
26398
|
+
function findJsVueVHtmlXssFindings(code, file) {
|
|
26399
|
+
const out2 = [];
|
|
26400
|
+
const lines = code.split("\n");
|
|
26401
|
+
const sourceRe = /\b(?:URLSearchParams|location\s*\.\s*(?:search|hash|href|pathname)|window\s*\.\s*location|route\s*\.\s*(?:query|params)|router\s*\.\s*(?:currentRoute|query)|\$route\s*\.\s*(?:query|params)|fetch\s*\(|axios\s*\.\s*(?:get|post)|XMLHttpRequest|document\s*\.\s*location)\b/;
|
|
26402
|
+
const tplRe = /\btemplate\s*:\s*(['"`])([\s\S]*?)\1/g;
|
|
26403
|
+
let tm;
|
|
26404
|
+
const boundVars = /* @__PURE__ */ new Set();
|
|
26405
|
+
while ((tm = tplRe.exec(code)) !== null) {
|
|
26406
|
+
const tpl = tm[2];
|
|
26407
|
+
const vhRe = /\bv-html\s*=\s*"([^"]+)"/g;
|
|
26408
|
+
let vm;
|
|
26409
|
+
while ((vm = vhRe.exec(tpl)) !== null) {
|
|
26410
|
+
const expr = vm[1].trim();
|
|
26411
|
+
const idMatch = expr.match(/^([A-Za-z_$][\w$]*)/);
|
|
26412
|
+
if (idMatch) boundVars.add(idMatch[1]);
|
|
26413
|
+
}
|
|
26414
|
+
}
|
|
26415
|
+
if (boundVars.size === 0) return out2;
|
|
26416
|
+
const taintedSet = /* @__PURE__ */ new Set();
|
|
26417
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
26418
|
+
let am;
|
|
26419
|
+
assignRe.lastIndex = 0;
|
|
26420
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26421
|
+
if (sourceRe.test(am[2])) taintedSet.add(am[1]);
|
|
26422
|
+
}
|
|
26423
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
26424
|
+
assignRe.lastIndex = 0;
|
|
26425
|
+
const before = taintedSet.size;
|
|
26426
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26427
|
+
if (taintedSet.has(am[1])) continue;
|
|
26428
|
+
for (const tv of taintedSet) {
|
|
26429
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
26430
|
+
taintedSet.add(am[1]);
|
|
26431
|
+
break;
|
|
26432
|
+
}
|
|
26433
|
+
}
|
|
26434
|
+
}
|
|
26435
|
+
if (taintedSet.size === before) break;
|
|
26436
|
+
}
|
|
26437
|
+
const taintedBindings = /* @__PURE__ */ new Set();
|
|
26438
|
+
for (const v of boundVars) {
|
|
26439
|
+
const bindRe = new RegExp(`\\b${v}\\s*[:=]\\s*([^,;\\n]+)`);
|
|
26440
|
+
for (const line of lines) {
|
|
26441
|
+
const m = line.match(bindRe);
|
|
26442
|
+
if (!m) continue;
|
|
26443
|
+
const val = m[1];
|
|
26444
|
+
if (sourceRe.test(val)) {
|
|
26445
|
+
taintedBindings.add(v);
|
|
26446
|
+
break;
|
|
26447
|
+
}
|
|
26448
|
+
let found = false;
|
|
26449
|
+
for (const tv of taintedSet) {
|
|
26450
|
+
if (new RegExp(`\\b${tv}\\b`).test(val)) {
|
|
26451
|
+
taintedBindings.add(v);
|
|
26452
|
+
found = true;
|
|
26453
|
+
break;
|
|
26454
|
+
}
|
|
26455
|
+
}
|
|
26456
|
+
if (found) break;
|
|
26457
|
+
}
|
|
26458
|
+
const propsRe = new RegExp(
|
|
26459
|
+
`\\bprops\\s*:\\s*\\[[^\\]]*['"\`]${v}['"\`][^\\]]*\\]`
|
|
26460
|
+
);
|
|
26461
|
+
if (propsRe.test(code)) taintedBindings.add(v);
|
|
26462
|
+
}
|
|
26463
|
+
if (taintedBindings.size === 0) return out2;
|
|
26464
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26465
|
+
const raw = lines[i2];
|
|
26466
|
+
if (!/v-html\s*=/.test(raw)) continue;
|
|
26467
|
+
const vm = raw.match(/v-html\s*=\s*"([^"]+)"/);
|
|
26468
|
+
if (!vm) continue;
|
|
26469
|
+
const idMatch = vm[1].trim().match(/^([A-Za-z_$][\w$]*)/);
|
|
26470
|
+
if (!idMatch || !taintedBindings.has(idMatch[1])) continue;
|
|
26471
|
+
out2.push({
|
|
26472
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
26473
|
+
pass: "language-sources",
|
|
26474
|
+
category: "security",
|
|
26475
|
+
rule_id: "xss",
|
|
26476
|
+
cwe: "CWE-79",
|
|
26477
|
+
severity: "high",
|
|
26478
|
+
level: "error",
|
|
26479
|
+
message: 'Vue v-html XSS: directive binds to "' + idMatch[1] + '" which is sourced from user-controlled input. Use {{ }} interpolation (auto-escapes) or sanitize the HTML with DOMPurify before binding.',
|
|
26480
|
+
file,
|
|
26481
|
+
line: i2 + 1,
|
|
26482
|
+
snippet: raw.trim().substring(0, 100)
|
|
26483
|
+
});
|
|
26484
|
+
}
|
|
26485
|
+
return out2;
|
|
26486
|
+
}
|
|
26487
|
+
function findTsAngularBypassXssFindings(code, file) {
|
|
26488
|
+
const out2 = [];
|
|
26489
|
+
const lines = code.split("\n");
|
|
26490
|
+
const sanitizerNames = /* @__PURE__ */ new Set();
|
|
26491
|
+
const declRe = /\b(?:public|private|protected|readonly|\s)?\s*([A-Za-z_$][\w$]*)\s*:\s*DomSanitizer\b/g;
|
|
26492
|
+
let m;
|
|
26493
|
+
declRe.lastIndex = 0;
|
|
26494
|
+
while ((m = declRe.exec(code)) !== null) sanitizerNames.add(m[1]);
|
|
26495
|
+
if (sanitizerNames.size === 0) return out2;
|
|
26496
|
+
const assignRe = /\bthis\s*\.\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\b/g;
|
|
26497
|
+
let am;
|
|
26498
|
+
assignRe.lastIndex = 0;
|
|
26499
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26500
|
+
if (sanitizerNames.has(am[2])) sanitizerNames.add(am[1]);
|
|
26501
|
+
}
|
|
26502
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26503
|
+
const raw = lines[i2];
|
|
26504
|
+
const trimmed = raw.trim();
|
|
26505
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
26506
|
+
const callRe = /\b(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\.\s*bypassSecurityTrust(Html|Script|Url|ResourceUrl|Style)\s*\(([\s\S]*?)\)/;
|
|
26507
|
+
const cm = trimmed.match(callRe);
|
|
26508
|
+
if (!cm) continue;
|
|
26509
|
+
const recv = cm[1];
|
|
26510
|
+
if (!sanitizerNames.has(recv)) continue;
|
|
26511
|
+
const arg = cm[3].trim();
|
|
26512
|
+
if (/^(['"`])[^'"`]*\1$/.test(arg)) continue;
|
|
26513
|
+
out2.push({
|
|
26514
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
26515
|
+
pass: "language-sources",
|
|
26516
|
+
category: "security",
|
|
26517
|
+
rule_id: "xss",
|
|
26518
|
+
cwe: "CWE-79",
|
|
26519
|
+
severity: "high",
|
|
26520
|
+
level: "error",
|
|
26521
|
+
message: "Angular XSS: DomSanitizer.bypassSecurityTrust" + cm[2] + "() opts out of Angular's built-in sanitization for a non-literal argument. Prefer leaving Angular's default sanitizer in place, or sanitize the input with DOMPurify before bypassing.",
|
|
26522
|
+
file,
|
|
26523
|
+
line: i2 + 1,
|
|
26524
|
+
snippet: trimmed.substring(0, 100)
|
|
26525
|
+
});
|
|
26526
|
+
}
|
|
26527
|
+
return out2;
|
|
26528
|
+
}
|
|
26529
|
+
function findPythonFlaskStringConcatXssFindings(code, file) {
|
|
26530
|
+
const out2 = [];
|
|
26531
|
+
const lines = code.split("\n");
|
|
26532
|
+
const fns = [];
|
|
26533
|
+
const defRe = /^(\s*)def\s+([A-Za-z_]\w*)\s*\(/;
|
|
26534
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26535
|
+
const raw = lines[i2];
|
|
26536
|
+
const dm = raw.match(defRe);
|
|
26537
|
+
if (!dm) continue;
|
|
26538
|
+
const indent = dm[1].length;
|
|
26539
|
+
let hasRoute = false;
|
|
26540
|
+
for (let j = i2 - 1; j >= 0; j--) {
|
|
26541
|
+
const prev = lines[j].trim();
|
|
26542
|
+
if (prev === "" || prev.startsWith("#")) continue;
|
|
26543
|
+
if (!prev.startsWith("@")) break;
|
|
26544
|
+
if (/@\s*[A-Za-z_]\w*\s*\.\s*route\s*\(/.test(prev) || /@\s*route\s*\(/.test(prev)) {
|
|
26545
|
+
hasRoute = true;
|
|
26546
|
+
}
|
|
26547
|
+
}
|
|
26548
|
+
if (!hasRoute) continue;
|
|
26549
|
+
let end = lines.length;
|
|
26550
|
+
for (let k = i2 + 1; k < lines.length; k++) {
|
|
26551
|
+
const ln = lines[k];
|
|
26552
|
+
if (!ln.trim()) continue;
|
|
26553
|
+
const ind = ln.match(/^(\s*)/)?.[1].length ?? 0;
|
|
26554
|
+
if (ind <= indent) {
|
|
26555
|
+
end = k;
|
|
26556
|
+
break;
|
|
26557
|
+
}
|
|
26558
|
+
}
|
|
26559
|
+
fns.push({ name: dm[2], startLine: i2 + 1, endLine: end });
|
|
26560
|
+
}
|
|
26561
|
+
if (fns.length === 0) return out2;
|
|
26562
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
26563
|
+
const escapeWrapRe = /\b(?:html\.escape|markupsafe\.escape|escape|markupsafe\.Markup\s*\.\s*escape|werkzeug\.utils\.escape)\s*\(/;
|
|
26564
|
+
for (const fn of fns) {
|
|
26565
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26566
|
+
for (let li = fn.startLine; li < fn.endLine; li++) {
|
|
26567
|
+
const raw = lines[li];
|
|
26568
|
+
const rm = raw.match(requestSourceRe);
|
|
26569
|
+
if (rm) taintedVars.add(rm[1]);
|
|
26570
|
+
}
|
|
26571
|
+
for (let li = fn.startLine; li < fn.endLine; li++) {
|
|
26572
|
+
const raw = lines[li];
|
|
26573
|
+
const trimmed = raw.trim();
|
|
26574
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26575
|
+
const retMatch = trimmed.match(/^(?:return|yield)\s+(.+)$/);
|
|
26576
|
+
if (!retMatch) continue;
|
|
26577
|
+
const expr = retMatch[1];
|
|
26578
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
|
|
26579
|
+
expr
|
|
26580
|
+
);
|
|
26581
|
+
const hasTaintedVar = [...taintedVars].some(
|
|
26582
|
+
(v) => new RegExp(`\\b${v}\\b`).test(expr)
|
|
26583
|
+
);
|
|
26584
|
+
if (!hasTaintedVar && !inlineSource) continue;
|
|
26585
|
+
if (escapeWrapRe.test(expr)) continue;
|
|
26586
|
+
const buildsHtml = /['"`]\s*\+/.test(expr) || /\+\s*['"`]/.test(expr) || // f-string with interpolation. Allow any non-`{` char between the
|
|
26587
|
+
// opening quote and the first `{` (Python f-strings may embed `"`
|
|
26588
|
+
// when single-quoted and vice-versa, e.g. f'<a href="{u}">').
|
|
26589
|
+
/\bf['"`][^{\n]*\{[^}]+\}/.test(expr) || /['"`]\s*%\s*[^=]/.test(expr) || /['"`]\.\s*format\s*\(/.test(expr);
|
|
26590
|
+
if (!buildsHtml) continue;
|
|
26591
|
+
out2.push({
|
|
26592
|
+
id: `xss-${file}-${li + 1}`,
|
|
26593
|
+
pass: "language-sources",
|
|
26594
|
+
category: "security",
|
|
26595
|
+
rule_id: "xss",
|
|
26596
|
+
cwe: "CWE-79",
|
|
26597
|
+
severity: "high",
|
|
26598
|
+
level: "error",
|
|
26599
|
+
message: "Reflected XSS: Flask route returns HTML built from request input via string concatenation / f-string / format. Wrap user input with markupsafe.escape() or render via a Jinja2 template (autoescape on by default).",
|
|
26600
|
+
file,
|
|
26601
|
+
line: li + 1,
|
|
26602
|
+
snippet: trimmed.substring(0, 100)
|
|
26603
|
+
});
|
|
26604
|
+
break;
|
|
26605
|
+
}
|
|
26606
|
+
}
|
|
26607
|
+
return out2;
|
|
26608
|
+
}
|
|
26609
|
+
function findPythonJinjaMarkupXssFindings(code, file) {
|
|
26610
|
+
const out2 = [];
|
|
26611
|
+
const lines = code.split("\n");
|
|
26612
|
+
const importRe = /^\s*from\s+(?:markupsafe|flask)\s+import\s+(?:[^#\n]*\b)?Markup\b/m;
|
|
26613
|
+
const classDefRe = /^\s*class\s+Markup\b/m;
|
|
26614
|
+
if (!importRe.test(code)) return out2;
|
|
26615
|
+
if (classDefRe.test(code)) return out2;
|
|
26616
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
26617
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26618
|
+
for (const line of lines) {
|
|
26619
|
+
const m = line.match(requestSourceRe);
|
|
26620
|
+
if (m) taintedVars.add(m[1]);
|
|
26621
|
+
}
|
|
26622
|
+
if (taintedVars.size === 0) return out2;
|
|
26623
|
+
if (!/\.\s*render\s*\(/.test(code)) return out2;
|
|
26624
|
+
const markupCallRe = /\bMarkup\s*\(\s*([A-Za-z_]\w*)\s*[,)]/g;
|
|
26625
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26626
|
+
const raw = lines[i2];
|
|
26627
|
+
const trimmed = raw.trim();
|
|
26628
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26629
|
+
let mm;
|
|
26630
|
+
markupCallRe.lastIndex = 0;
|
|
26631
|
+
while ((mm = markupCallRe.exec(trimmed)) !== null) {
|
|
26632
|
+
const argName = mm[1];
|
|
26633
|
+
if (!taintedVars.has(argName)) continue;
|
|
26634
|
+
out2.push({
|
|
26635
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
26636
|
+
pass: "language-sources",
|
|
26637
|
+
category: "security",
|
|
26638
|
+
rule_id: "xss",
|
|
26639
|
+
cwe: "CWE-79",
|
|
26640
|
+
severity: "high",
|
|
26641
|
+
level: "error",
|
|
26642
|
+
message: "Jinja2 autoescape bypass: Markup(" + argName + ") wraps a request-derived value, disabling autoescape when rendered. Pass the raw value to render() and let Jinja2 auto-escape, or sanitize with bleach.clean first.",
|
|
26643
|
+
file,
|
|
26644
|
+
line: i2 + 1,
|
|
26645
|
+
snippet: trimmed.substring(0, 100)
|
|
26646
|
+
});
|
|
26647
|
+
break;
|
|
26648
|
+
}
|
|
26649
|
+
}
|
|
26650
|
+
return out2;
|
|
26651
|
+
}
|
|
26652
|
+
function findGoLocationHeaderOpenRedirectFindings(code, file) {
|
|
26653
|
+
const out2 = [];
|
|
26654
|
+
const lines = code.split("\n");
|
|
26655
|
+
const rwNames = /* @__PURE__ */ new Set();
|
|
26656
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
26657
|
+
for (const line of lines) {
|
|
26658
|
+
let m;
|
|
26659
|
+
sigRe.lastIndex = 0;
|
|
26660
|
+
while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
|
|
26661
|
+
}
|
|
26662
|
+
if (rwNames.size === 0) return out2;
|
|
26663
|
+
const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
|
|
26664
|
+
const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
26665
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26666
|
+
const raw = lines[i2];
|
|
26667
|
+
const trimmed = raw.trim();
|
|
26668
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26669
|
+
const m = trimmed.match(callRe);
|
|
26670
|
+
if (!m) continue;
|
|
26671
|
+
const recv = m[1];
|
|
26672
|
+
if (!rwNames.has(recv)) continue;
|
|
26673
|
+
const key = m[3];
|
|
26674
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26675
|
+
const valExpr = m[4];
|
|
26676
|
+
const valTrim = valExpr.trim();
|
|
26677
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(valTrim)) continue;
|
|
26678
|
+
if (safeWrapRe.test(valExpr)) continue;
|
|
26679
|
+
out2.push({
|
|
26680
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26681
|
+
pass: "language-sources",
|
|
26682
|
+
category: "security",
|
|
26683
|
+
rule_id: "open_redirect",
|
|
26684
|
+
cwe: "CWE-601",
|
|
26685
|
+
severity: "high",
|
|
26686
|
+
level: "error",
|
|
26687
|
+
message: 'Open redirect: ResponseWriter.Header().Set("Location", ...) writes a user-controlled value into the Location header without validating the target. Restrict to an allow-list of hosts/paths or compare against a known-safe set before issuing the redirect.',
|
|
26688
|
+
file,
|
|
26689
|
+
line: i2 + 1,
|
|
26690
|
+
snippet: trimmed.substring(0, 100)
|
|
26691
|
+
});
|
|
26692
|
+
}
|
|
26693
|
+
return out2;
|
|
26694
|
+
}
|
|
26695
|
+
function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
|
|
26696
|
+
const out2 = [];
|
|
26697
|
+
const lines = code.split("\n");
|
|
26698
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
26699
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26700
|
+
for (const line of lines) {
|
|
26701
|
+
const m = line.match(requestSourceRe);
|
|
26702
|
+
if (m) taintedVars.add(m[1]);
|
|
26703
|
+
}
|
|
26704
|
+
const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
|
|
26705
|
+
const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
|
|
26706
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26707
|
+
const raw = lines[i2];
|
|
26708
|
+
const trimmed = raw.trim();
|
|
26709
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26710
|
+
const m = trimmed.match(subscriptRe);
|
|
26711
|
+
if (!m) continue;
|
|
26712
|
+
const key = m[3];
|
|
26713
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26714
|
+
const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
|
|
26715
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs)) continue;
|
|
26716
|
+
if (safeWrapRe.test(rhs)) continue;
|
|
26717
|
+
const hasTaintedVar = [...taintedVars].some(
|
|
26718
|
+
(v) => new RegExp(`\\b${v}\\b`).test(rhs)
|
|
26719
|
+
);
|
|
26720
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
|
|
26721
|
+
rhs
|
|
26722
|
+
);
|
|
26723
|
+
if (!hasTaintedVar && !inlineSource) continue;
|
|
26724
|
+
out2.push({
|
|
26725
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26726
|
+
pass: "language-sources",
|
|
26727
|
+
category: "security",
|
|
26728
|
+
rule_id: "open_redirect",
|
|
26729
|
+
cwe: "CWE-601",
|
|
26730
|
+
severity: "high",
|
|
26731
|
+
level: "error",
|
|
26732
|
+
message: 'Open redirect: response.headers["Location"] = ... assigns a user-controlled value to the Location header. Validate the URL against an allow-list of trusted hosts before sending.',
|
|
26733
|
+
file,
|
|
26734
|
+
line: i2 + 1,
|
|
26735
|
+
snippet: trimmed.substring(0, 100)
|
|
26736
|
+
});
|
|
26737
|
+
}
|
|
26738
|
+
return out2;
|
|
26739
|
+
}
|
|
26740
|
+
function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
|
|
26741
|
+
const out2 = [];
|
|
26742
|
+
const lines = code.split("\n");
|
|
26743
|
+
const extractorRe = /\b([A-Za-z_]\w*)\s*:\s*(?:web\s*::\s*)?(?:Query|Path|Form|Json)\s*<|\b([A-Za-z_]\w*)\s*:\s*&?\s*HttpRequest\b/g;
|
|
26744
|
+
const extractorParams = /* @__PURE__ */ new Set();
|
|
26745
|
+
for (const line of lines) {
|
|
26746
|
+
let m;
|
|
26747
|
+
extractorRe.lastIndex = 0;
|
|
26748
|
+
while ((m = extractorRe.exec(line)) !== null) {
|
|
26749
|
+
const name2 = m[1] ?? m[2];
|
|
26750
|
+
if (name2) extractorParams.add(name2);
|
|
26751
|
+
}
|
|
26752
|
+
}
|
|
26753
|
+
if (extractorParams.size === 0) return out2;
|
|
26754
|
+
const taintedVars = new Set(extractorParams);
|
|
26755
|
+
const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
|
|
26756
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
26757
|
+
const before = taintedVars.size;
|
|
26758
|
+
let lm;
|
|
26759
|
+
letRe.lastIndex = 0;
|
|
26760
|
+
while ((lm = letRe.exec(code)) !== null) {
|
|
26761
|
+
const name2 = lm[1];
|
|
26762
|
+
const rhs = lm[2];
|
|
26763
|
+
if (taintedVars.has(name2)) continue;
|
|
26764
|
+
for (const tv of taintedVars) {
|
|
26765
|
+
if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
|
|
26766
|
+
taintedVars.add(name2);
|
|
26767
|
+
break;
|
|
26768
|
+
}
|
|
26769
|
+
}
|
|
26770
|
+
}
|
|
26771
|
+
if (taintedVars.size === before) break;
|
|
26772
|
+
}
|
|
26773
|
+
const safeWrapRe = /\b(?:Url\s*::\s*parse|url\s*::\s*Url\s*::\s*parse)\s*\([\s\S]{0,160}?\b(?:host_str|host|origin)\b/;
|
|
26774
|
+
const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
|
|
26775
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26776
|
+
const raw = lines[i2];
|
|
26777
|
+
const trimmed = raw.trim();
|
|
26778
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26779
|
+
let cm;
|
|
26780
|
+
tupleCallRe.lastIndex = 0;
|
|
26781
|
+
while ((cm = tupleCallRe.exec(raw)) !== null) {
|
|
26782
|
+
const key = cm[2];
|
|
26783
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26784
|
+
const valExpr = cm[3];
|
|
26785
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim())) continue;
|
|
26786
|
+
if (safeWrapRe.test(valExpr)) continue;
|
|
26787
|
+
const hasTainted = [...taintedVars].some(
|
|
26788
|
+
(v) => new RegExp(`\\b${v}\\b`).test(valExpr)
|
|
26789
|
+
);
|
|
26790
|
+
if (!hasTainted) continue;
|
|
26791
|
+
out2.push({
|
|
26792
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26793
|
+
pass: "language-sources",
|
|
26794
|
+
category: "security",
|
|
26795
|
+
rule_id: "open_redirect",
|
|
26796
|
+
cwe: "CWE-601",
|
|
26797
|
+
severity: "high",
|
|
26798
|
+
level: "error",
|
|
26799
|
+
message: 'Open redirect: HttpResponse builder .append_header(("Location", ...)) sets the Location header from a request-derived value without allow-list validation. Parse with url::Url::parse and compare host_str against an allow-list.',
|
|
26800
|
+
file,
|
|
26801
|
+
line: i2 + 1,
|
|
26802
|
+
snippet: trimmed.substring(0, 100)
|
|
26803
|
+
});
|
|
26804
|
+
break;
|
|
26805
|
+
}
|
|
26806
|
+
}
|
|
26807
|
+
return out2;
|
|
26808
|
+
}
|
|
26809
|
+
function findJsDomOpenRedirectFindings(code, file) {
|
|
26810
|
+
const out2 = [];
|
|
26811
|
+
const lines = code.split("\n");
|
|
26812
|
+
const domSourceRe = /\blocation\s*\.\s*(?:search|hash|href|pathname)\b|\bwindow\s*\.\s*location\s*\.\s*(?:search|hash|href|pathname)\b|\bURLSearchParams\b|\bdocument\s*\.\s*(?:referrer|URL|location\s*\.\s*(?:search|hash|href|pathname))\b|\bwindow\s*\.\s*name\b/;
|
|
26813
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26814
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
26815
|
+
let am;
|
|
26816
|
+
assignRe.lastIndex = 0;
|
|
26817
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26818
|
+
if (domSourceRe.test(am[2])) taintedVars.add(am[1]);
|
|
26819
|
+
}
|
|
26820
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
26821
|
+
const before = taintedVars.size;
|
|
26822
|
+
assignRe.lastIndex = 0;
|
|
26823
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26824
|
+
if (taintedVars.has(am[1])) continue;
|
|
26825
|
+
for (const tv of taintedVars) {
|
|
26826
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
26827
|
+
taintedVars.add(am[1]);
|
|
26828
|
+
break;
|
|
26829
|
+
}
|
|
26830
|
+
}
|
|
26831
|
+
}
|
|
26832
|
+
if (taintedVars.size === before) break;
|
|
26833
|
+
}
|
|
26834
|
+
const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
|
|
26835
|
+
const containsTaint = (expr) => {
|
|
26836
|
+
if (domSourceRe.test(expr)) return true;
|
|
26837
|
+
for (const tv of taintedVars) {
|
|
26838
|
+
if (new RegExp(`\\b${tv}\\b`).test(expr)) return true;
|
|
26839
|
+
}
|
|
26840
|
+
return false;
|
|
26841
|
+
};
|
|
26842
|
+
const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26843
|
+
const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26844
|
+
const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
|
|
26845
|
+
const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
|
|
26846
|
+
const emit = (line, msg, snippet) => {
|
|
26847
|
+
out2.push({
|
|
26848
|
+
id: `open_redirect-${file}-${line}`,
|
|
26849
|
+
pass: "language-sources",
|
|
26850
|
+
category: "security",
|
|
26851
|
+
rule_id: "open_redirect",
|
|
26852
|
+
cwe: "CWE-601",
|
|
26853
|
+
severity: "high",
|
|
26854
|
+
level: "error",
|
|
26855
|
+
message: msg,
|
|
26856
|
+
file,
|
|
26857
|
+
line,
|
|
26858
|
+
snippet: snippet.substring(0, 100)
|
|
26859
|
+
});
|
|
26860
|
+
};
|
|
26861
|
+
const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
|
|
26862
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26863
|
+
const raw = lines[i2];
|
|
26864
|
+
const trimmed = raw.trim();
|
|
26865
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26866
|
+
let matched = false;
|
|
26867
|
+
const hm = trimmed.match(hrefSinkRe);
|
|
26868
|
+
if (hm) {
|
|
26869
|
+
const rhs = hm[1].trim();
|
|
26870
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26871
|
+
emit(
|
|
26872
|
+
i2 + 1,
|
|
26873
|
+
"Open redirect: assignment to location.href uses a value derived from a URL query/hash without allow-list validation. Compare the target origin to a known-safe set before navigating.",
|
|
26874
|
+
trimmed
|
|
26875
|
+
);
|
|
26876
|
+
matched = true;
|
|
26877
|
+
}
|
|
26878
|
+
}
|
|
26879
|
+
if (!matched) {
|
|
26880
|
+
const lm = trimmed.match(locSinkRe);
|
|
26881
|
+
if (lm) {
|
|
26882
|
+
const rhs = lm[1].trim();
|
|
26883
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26884
|
+
emit(
|
|
26885
|
+
i2 + 1,
|
|
26886
|
+
"Open redirect: assignment to window.location uses a value derived from a URL query/hash without allow-list validation. Compare the target origin to a known-safe set before navigating.",
|
|
26887
|
+
trimmed
|
|
26888
|
+
);
|
|
26889
|
+
matched = true;
|
|
26890
|
+
}
|
|
26891
|
+
}
|
|
26892
|
+
}
|
|
26893
|
+
if (!matched) {
|
|
26894
|
+
const cm = trimmed.match(contentSinkRe);
|
|
26895
|
+
if (cm) {
|
|
26896
|
+
const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
|
|
26897
|
+
if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26898
|
+
emit(
|
|
26899
|
+
i2 + 1,
|
|
26900
|
+
"Open redirect: DOM assignment to <meta>.content with a meta-refresh URL built from a URL query/hash without allow-list validation. Validate origin before navigating.",
|
|
26901
|
+
trimmed
|
|
26902
|
+
);
|
|
26903
|
+
matched = true;
|
|
26904
|
+
}
|
|
26905
|
+
}
|
|
26906
|
+
}
|
|
26907
|
+
if (!matched) {
|
|
26908
|
+
const callm = trimmed.match(callSinkRe);
|
|
26909
|
+
if (callm) {
|
|
26910
|
+
const arg = callm[1].trim();
|
|
26911
|
+
if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
|
|
26912
|
+
emit(
|
|
26913
|
+
i2 + 1,
|
|
26914
|
+
"Open redirect: location.assign / location.replace invoked with a value derived from a URL query/hash without allow-list validation. Validate origin before navigating.",
|
|
26915
|
+
trimmed
|
|
26916
|
+
);
|
|
26917
|
+
}
|
|
26918
|
+
}
|
|
26919
|
+
}
|
|
26920
|
+
}
|
|
26921
|
+
return out2;
|
|
26922
|
+
}
|
|
26215
26923
|
|
|
26216
26924
|
// src/analysis/passes/sink-filter-pass.ts
|
|
26217
26925
|
var JS_XSS_SANITIZERS = [
|
|
@@ -27546,13 +28254,13 @@ var TaintPropagationPass = class {
|
|
|
27546
28254
|
}));
|
|
27547
28255
|
const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
|
|
27548
28256
|
for (const f of arrayFlows) {
|
|
27549
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28257
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27550
28258
|
flows.push(f);
|
|
27551
28259
|
}
|
|
27552
28260
|
}
|
|
27553
28261
|
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
|
|
27554
28262
|
for (const f of collectionFlows) {
|
|
27555
|
-
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
|
|
28263
|
+
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) continue;
|
|
27556
28264
|
const flowForCheck = {
|
|
27557
28265
|
source: { line: f.source_line },
|
|
27558
28266
|
sink: { line: f.sink_line },
|
|
@@ -27571,7 +28279,7 @@ var TaintPropagationPass = class {
|
|
|
27571
28279
|
}
|
|
27572
28280
|
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
27573
28281
|
for (const f of paramFlows) {
|
|
27574
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28282
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27575
28283
|
flows.push(f);
|
|
27576
28284
|
}
|
|
27577
28285
|
}
|
|
@@ -27961,7 +28669,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
27961
28669
|
}
|
|
27962
28670
|
const paramSource = methodParamSources.get(arg.variable);
|
|
27963
28671
|
if (paramSource) {
|
|
27964
|
-
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
28672
|
+
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
27965
28673
|
if (!exists) {
|
|
27966
28674
|
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
27967
28675
|
continue;
|