circle-ir 3.131.0 → 3.133.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 +102 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +974 -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 +756 -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
|
};
|
|
@@ -23657,6 +23666,18 @@ var LanguageSourcesPass = class {
|
|
|
23657
23666
|
for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
|
|
23658
23667
|
ctx.addFinding(finding);
|
|
23659
23668
|
}
|
|
23669
|
+
for (const finding of findGoLocationHeaderOpenRedirectFindings(
|
|
23670
|
+
code,
|
|
23671
|
+
graph.ir.meta.file
|
|
23672
|
+
)) {
|
|
23673
|
+
ctx.addFinding(finding);
|
|
23674
|
+
}
|
|
23675
|
+
for (const finding of findGoPluginOpenCodeInjectionFindings(
|
|
23676
|
+
code,
|
|
23677
|
+
graph.ir.meta.file
|
|
23678
|
+
)) {
|
|
23679
|
+
ctx.addFinding(finding);
|
|
23680
|
+
}
|
|
23660
23681
|
}
|
|
23661
23682
|
if (language === "python") {
|
|
23662
23683
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -23675,6 +23696,18 @@ var LanguageSourcesPass = class {
|
|
|
23675
23696
|
for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
|
|
23676
23697
|
ctx.addFinding(finding);
|
|
23677
23698
|
}
|
|
23699
|
+
for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(
|
|
23700
|
+
code,
|
|
23701
|
+
graph.ir.meta.file
|
|
23702
|
+
)) {
|
|
23703
|
+
ctx.addFinding(finding);
|
|
23704
|
+
}
|
|
23705
|
+
for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(
|
|
23706
|
+
code,
|
|
23707
|
+
graph.ir.meta.file
|
|
23708
|
+
)) {
|
|
23709
|
+
ctx.addFinding(finding);
|
|
23710
|
+
}
|
|
23678
23711
|
}
|
|
23679
23712
|
if (language === "rust") {
|
|
23680
23713
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -23696,6 +23729,18 @@ var LanguageSourcesPass = class {
|
|
|
23696
23729
|
for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
|
|
23697
23730
|
ctx.addFinding(finding);
|
|
23698
23731
|
}
|
|
23732
|
+
for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(
|
|
23733
|
+
code,
|
|
23734
|
+
graph.ir.meta.file
|
|
23735
|
+
)) {
|
|
23736
|
+
ctx.addFinding(finding);
|
|
23737
|
+
}
|
|
23738
|
+
for (const finding of findRustEvalCrateCodeInjectionFindings(
|
|
23739
|
+
code,
|
|
23740
|
+
graph.ir.meta.file
|
|
23741
|
+
)) {
|
|
23742
|
+
ctx.addFinding(finding);
|
|
23743
|
+
}
|
|
23699
23744
|
}
|
|
23700
23745
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
23701
23746
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -23714,6 +23759,18 @@ var LanguageSourcesPass = class {
|
|
|
23714
23759
|
for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
|
|
23715
23760
|
ctx.addFinding(finding);
|
|
23716
23761
|
}
|
|
23762
|
+
for (const finding of findJsDomOpenRedirectFindings(
|
|
23763
|
+
code,
|
|
23764
|
+
graph.ir.meta.file
|
|
23765
|
+
)) {
|
|
23766
|
+
ctx.addFinding(finding);
|
|
23767
|
+
}
|
|
23768
|
+
for (const finding of findJsIndirectEvalCodeInjectionFindings(
|
|
23769
|
+
code,
|
|
23770
|
+
graph.ir.meta.file
|
|
23771
|
+
)) {
|
|
23772
|
+
ctx.addFinding(finding);
|
|
23773
|
+
}
|
|
23717
23774
|
}
|
|
23718
23775
|
if (language === "java") {
|
|
23719
23776
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -26616,6 +26673,691 @@ function findPythonJinjaMarkupXssFindings(code, file) {
|
|
|
26616
26673
|
}
|
|
26617
26674
|
return out2;
|
|
26618
26675
|
}
|
|
26676
|
+
function findGoLocationHeaderOpenRedirectFindings(code, file) {
|
|
26677
|
+
const out2 = [];
|
|
26678
|
+
const lines = code.split("\n");
|
|
26679
|
+
const rwNames = /* @__PURE__ */ new Set();
|
|
26680
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
26681
|
+
for (const line of lines) {
|
|
26682
|
+
let m;
|
|
26683
|
+
sigRe.lastIndex = 0;
|
|
26684
|
+
while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
|
|
26685
|
+
}
|
|
26686
|
+
if (rwNames.size === 0) return out2;
|
|
26687
|
+
const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
|
|
26688
|
+
const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
26689
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26690
|
+
const raw = lines[i2];
|
|
26691
|
+
const trimmed = raw.trim();
|
|
26692
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26693
|
+
const m = trimmed.match(callRe);
|
|
26694
|
+
if (!m) continue;
|
|
26695
|
+
const recv = m[1];
|
|
26696
|
+
if (!rwNames.has(recv)) continue;
|
|
26697
|
+
const key = m[3];
|
|
26698
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26699
|
+
const valExpr = m[4];
|
|
26700
|
+
const valTrim = valExpr.trim();
|
|
26701
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(valTrim)) continue;
|
|
26702
|
+
if (safeWrapRe.test(valExpr)) continue;
|
|
26703
|
+
out2.push({
|
|
26704
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26705
|
+
pass: "language-sources",
|
|
26706
|
+
category: "security",
|
|
26707
|
+
rule_id: "open_redirect",
|
|
26708
|
+
cwe: "CWE-601",
|
|
26709
|
+
severity: "high",
|
|
26710
|
+
level: "error",
|
|
26711
|
+
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.',
|
|
26712
|
+
file,
|
|
26713
|
+
line: i2 + 1,
|
|
26714
|
+
snippet: trimmed.substring(0, 100)
|
|
26715
|
+
});
|
|
26716
|
+
}
|
|
26717
|
+
return out2;
|
|
26718
|
+
}
|
|
26719
|
+
function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
|
|
26720
|
+
const out2 = [];
|
|
26721
|
+
const lines = code.split("\n");
|
|
26722
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
26723
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26724
|
+
for (const line of lines) {
|
|
26725
|
+
const m = line.match(requestSourceRe);
|
|
26726
|
+
if (m) taintedVars.add(m[1]);
|
|
26727
|
+
}
|
|
26728
|
+
const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
|
|
26729
|
+
const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
|
|
26730
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26731
|
+
const raw = lines[i2];
|
|
26732
|
+
const trimmed = raw.trim();
|
|
26733
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26734
|
+
const m = trimmed.match(subscriptRe);
|
|
26735
|
+
if (!m) continue;
|
|
26736
|
+
const key = m[3];
|
|
26737
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26738
|
+
const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
|
|
26739
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs)) continue;
|
|
26740
|
+
if (safeWrapRe.test(rhs)) continue;
|
|
26741
|
+
const hasTaintedVar = [...taintedVars].some(
|
|
26742
|
+
(v) => new RegExp(`\\b${v}\\b`).test(rhs)
|
|
26743
|
+
);
|
|
26744
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
|
|
26745
|
+
rhs
|
|
26746
|
+
);
|
|
26747
|
+
if (!hasTaintedVar && !inlineSource) continue;
|
|
26748
|
+
out2.push({
|
|
26749
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26750
|
+
pass: "language-sources",
|
|
26751
|
+
category: "security",
|
|
26752
|
+
rule_id: "open_redirect",
|
|
26753
|
+
cwe: "CWE-601",
|
|
26754
|
+
severity: "high",
|
|
26755
|
+
level: "error",
|
|
26756
|
+
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.',
|
|
26757
|
+
file,
|
|
26758
|
+
line: i2 + 1,
|
|
26759
|
+
snippet: trimmed.substring(0, 100)
|
|
26760
|
+
});
|
|
26761
|
+
}
|
|
26762
|
+
return out2;
|
|
26763
|
+
}
|
|
26764
|
+
function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
|
|
26765
|
+
const out2 = [];
|
|
26766
|
+
const lines = code.split("\n");
|
|
26767
|
+
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;
|
|
26768
|
+
const extractorParams = /* @__PURE__ */ new Set();
|
|
26769
|
+
for (const line of lines) {
|
|
26770
|
+
let m;
|
|
26771
|
+
extractorRe.lastIndex = 0;
|
|
26772
|
+
while ((m = extractorRe.exec(line)) !== null) {
|
|
26773
|
+
const name2 = m[1] ?? m[2];
|
|
26774
|
+
if (name2) extractorParams.add(name2);
|
|
26775
|
+
}
|
|
26776
|
+
}
|
|
26777
|
+
if (extractorParams.size === 0) return out2;
|
|
26778
|
+
const taintedVars = new Set(extractorParams);
|
|
26779
|
+
const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
|
|
26780
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
26781
|
+
const before = taintedVars.size;
|
|
26782
|
+
let lm;
|
|
26783
|
+
letRe.lastIndex = 0;
|
|
26784
|
+
while ((lm = letRe.exec(code)) !== null) {
|
|
26785
|
+
const name2 = lm[1];
|
|
26786
|
+
const rhs = lm[2];
|
|
26787
|
+
if (taintedVars.has(name2)) continue;
|
|
26788
|
+
for (const tv of taintedVars) {
|
|
26789
|
+
if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
|
|
26790
|
+
taintedVars.add(name2);
|
|
26791
|
+
break;
|
|
26792
|
+
}
|
|
26793
|
+
}
|
|
26794
|
+
}
|
|
26795
|
+
if (taintedVars.size === before) break;
|
|
26796
|
+
}
|
|
26797
|
+
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/;
|
|
26798
|
+
const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
|
|
26799
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26800
|
+
const raw = lines[i2];
|
|
26801
|
+
const trimmed = raw.trim();
|
|
26802
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26803
|
+
let cm;
|
|
26804
|
+
tupleCallRe.lastIndex = 0;
|
|
26805
|
+
while ((cm = tupleCallRe.exec(raw)) !== null) {
|
|
26806
|
+
const key = cm[2];
|
|
26807
|
+
if (key.toLowerCase() !== "location") continue;
|
|
26808
|
+
const valExpr = cm[3];
|
|
26809
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim())) continue;
|
|
26810
|
+
if (safeWrapRe.test(valExpr)) continue;
|
|
26811
|
+
const hasTainted = [...taintedVars].some(
|
|
26812
|
+
(v) => new RegExp(`\\b${v}\\b`).test(valExpr)
|
|
26813
|
+
);
|
|
26814
|
+
if (!hasTainted) continue;
|
|
26815
|
+
out2.push({
|
|
26816
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26817
|
+
pass: "language-sources",
|
|
26818
|
+
category: "security",
|
|
26819
|
+
rule_id: "open_redirect",
|
|
26820
|
+
cwe: "CWE-601",
|
|
26821
|
+
severity: "high",
|
|
26822
|
+
level: "error",
|
|
26823
|
+
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.',
|
|
26824
|
+
file,
|
|
26825
|
+
line: i2 + 1,
|
|
26826
|
+
snippet: trimmed.substring(0, 100)
|
|
26827
|
+
});
|
|
26828
|
+
break;
|
|
26829
|
+
}
|
|
26830
|
+
}
|
|
26831
|
+
return out2;
|
|
26832
|
+
}
|
|
26833
|
+
function findJsDomOpenRedirectFindings(code, file) {
|
|
26834
|
+
const out2 = [];
|
|
26835
|
+
const lines = code.split("\n");
|
|
26836
|
+
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/;
|
|
26837
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26838
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
26839
|
+
let am;
|
|
26840
|
+
assignRe.lastIndex = 0;
|
|
26841
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26842
|
+
if (domSourceRe.test(am[2])) taintedVars.add(am[1]);
|
|
26843
|
+
}
|
|
26844
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
26845
|
+
const before = taintedVars.size;
|
|
26846
|
+
assignRe.lastIndex = 0;
|
|
26847
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26848
|
+
if (taintedVars.has(am[1])) continue;
|
|
26849
|
+
for (const tv of taintedVars) {
|
|
26850
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
26851
|
+
taintedVars.add(am[1]);
|
|
26852
|
+
break;
|
|
26853
|
+
}
|
|
26854
|
+
}
|
|
26855
|
+
}
|
|
26856
|
+
if (taintedVars.size === before) break;
|
|
26857
|
+
}
|
|
26858
|
+
const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
|
|
26859
|
+
const containsTaint = (expr) => {
|
|
26860
|
+
if (domSourceRe.test(expr)) return true;
|
|
26861
|
+
for (const tv of taintedVars) {
|
|
26862
|
+
if (new RegExp(`\\b${tv}\\b`).test(expr)) return true;
|
|
26863
|
+
}
|
|
26864
|
+
return false;
|
|
26865
|
+
};
|
|
26866
|
+
const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26867
|
+
const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26868
|
+
const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
|
|
26869
|
+
const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
|
|
26870
|
+
const emit = (line, msg, snippet) => {
|
|
26871
|
+
out2.push({
|
|
26872
|
+
id: `open_redirect-${file}-${line}`,
|
|
26873
|
+
pass: "language-sources",
|
|
26874
|
+
category: "security",
|
|
26875
|
+
rule_id: "open_redirect",
|
|
26876
|
+
cwe: "CWE-601",
|
|
26877
|
+
severity: "high",
|
|
26878
|
+
level: "error",
|
|
26879
|
+
message: msg,
|
|
26880
|
+
file,
|
|
26881
|
+
line,
|
|
26882
|
+
snippet: snippet.substring(0, 100)
|
|
26883
|
+
});
|
|
26884
|
+
};
|
|
26885
|
+
const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
|
|
26886
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26887
|
+
const raw = lines[i2];
|
|
26888
|
+
const trimmed = raw.trim();
|
|
26889
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
26890
|
+
let matched = false;
|
|
26891
|
+
const hm = trimmed.match(hrefSinkRe);
|
|
26892
|
+
if (hm) {
|
|
26893
|
+
const rhs = hm[1].trim();
|
|
26894
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26895
|
+
emit(
|
|
26896
|
+
i2 + 1,
|
|
26897
|
+
"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.",
|
|
26898
|
+
trimmed
|
|
26899
|
+
);
|
|
26900
|
+
matched = true;
|
|
26901
|
+
}
|
|
26902
|
+
}
|
|
26903
|
+
if (!matched) {
|
|
26904
|
+
const lm = trimmed.match(locSinkRe);
|
|
26905
|
+
if (lm) {
|
|
26906
|
+
const rhs = lm[1].trim();
|
|
26907
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26908
|
+
emit(
|
|
26909
|
+
i2 + 1,
|
|
26910
|
+
"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.",
|
|
26911
|
+
trimmed
|
|
26912
|
+
);
|
|
26913
|
+
matched = true;
|
|
26914
|
+
}
|
|
26915
|
+
}
|
|
26916
|
+
}
|
|
26917
|
+
if (!matched) {
|
|
26918
|
+
const cm = trimmed.match(contentSinkRe);
|
|
26919
|
+
if (cm) {
|
|
26920
|
+
const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
|
|
26921
|
+
if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26922
|
+
emit(
|
|
26923
|
+
i2 + 1,
|
|
26924
|
+
"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.",
|
|
26925
|
+
trimmed
|
|
26926
|
+
);
|
|
26927
|
+
matched = true;
|
|
26928
|
+
}
|
|
26929
|
+
}
|
|
26930
|
+
}
|
|
26931
|
+
if (!matched) {
|
|
26932
|
+
const callm = trimmed.match(callSinkRe);
|
|
26933
|
+
if (callm) {
|
|
26934
|
+
const arg = callm[1].trim();
|
|
26935
|
+
if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
|
|
26936
|
+
emit(
|
|
26937
|
+
i2 + 1,
|
|
26938
|
+
"Open redirect: location.assign / location.replace invoked with a value derived from a URL query/hash without allow-list validation. Validate origin before navigating.",
|
|
26939
|
+
trimmed
|
|
26940
|
+
);
|
|
26941
|
+
}
|
|
26942
|
+
}
|
|
26943
|
+
}
|
|
26944
|
+
}
|
|
26945
|
+
return out2;
|
|
26946
|
+
}
|
|
26947
|
+
function findGoPluginOpenCodeInjectionFindings(code, file) {
|
|
26948
|
+
const findings = [];
|
|
26949
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
26950
|
+
if (!/\bplugin\s*\.\s*(?:Open|Lookup)\s*\(/.test(code)) return findings;
|
|
26951
|
+
const lines = code.split("\n");
|
|
26952
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\.Query\(\)\.Get|Header\.Get|Cookie)\s*\(/;
|
|
26953
|
+
const httpReqParamRe = /\*\s*http\.Request\b/;
|
|
26954
|
+
const callRe = /\bplugin\s*\.\s*(?:Open|Lookup)\s*\(\s*([^)]*)\s*\)/;
|
|
26955
|
+
const sinkLabel = (op) => op === "Lookup" ? "Go plugin.Lookup" : "Go plugin.Open";
|
|
26956
|
+
const funcs = [];
|
|
26957
|
+
let cur = null;
|
|
26958
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26959
|
+
const t = lines[i2].trim();
|
|
26960
|
+
if (/^func\b/.test(t)) {
|
|
26961
|
+
if (cur) {
|
|
26962
|
+
cur.end = i2 - 1;
|
|
26963
|
+
funcs.push(cur);
|
|
26964
|
+
}
|
|
26965
|
+
cur = { start: i2, end: lines.length - 1 };
|
|
26966
|
+
}
|
|
26967
|
+
}
|
|
26968
|
+
if (cur) funcs.push(cur);
|
|
26969
|
+
for (const fn of funcs) {
|
|
26970
|
+
const header = lines[fn.start];
|
|
26971
|
+
if (!httpReqParamRe.test(header)) continue;
|
|
26972
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26973
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
26974
|
+
const before = taintedVars.size;
|
|
26975
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
26976
|
+
const line = lines[i2];
|
|
26977
|
+
const trimmed = line.trim();
|
|
26978
|
+
const assignMatch = trimmed.match(
|
|
26979
|
+
/^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/
|
|
26980
|
+
);
|
|
26981
|
+
if (!assignMatch) continue;
|
|
26982
|
+
const lhs = assignMatch[1];
|
|
26983
|
+
const rhs = assignMatch[2];
|
|
26984
|
+
if (taintedVars.has(lhs)) continue;
|
|
26985
|
+
if (reqExtractRe.test(rhs)) {
|
|
26986
|
+
taintedVars.add(lhs);
|
|
26987
|
+
continue;
|
|
26988
|
+
}
|
|
26989
|
+
for (const v of taintedVars) {
|
|
26990
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26991
|
+
if (re.test(rhs)) {
|
|
26992
|
+
taintedVars.add(lhs);
|
|
26993
|
+
break;
|
|
26994
|
+
}
|
|
26995
|
+
}
|
|
26996
|
+
}
|
|
26997
|
+
if (taintedVars.size === before) break;
|
|
26998
|
+
}
|
|
26999
|
+
if (taintedVars.size === 0) continue;
|
|
27000
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27001
|
+
const line = lines[i2];
|
|
27002
|
+
const m = line.match(callRe);
|
|
27003
|
+
if (!m) continue;
|
|
27004
|
+
const arg = m[1].trim();
|
|
27005
|
+
if (arg.length === 0) continue;
|
|
27006
|
+
if (/^"[^"]*"$/.test(arg)) continue;
|
|
27007
|
+
let tainted = false;
|
|
27008
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27009
|
+
else {
|
|
27010
|
+
for (const v of taintedVars) {
|
|
27011
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27012
|
+
if (re.test(arg)) {
|
|
27013
|
+
tainted = true;
|
|
27014
|
+
break;
|
|
27015
|
+
}
|
|
27016
|
+
}
|
|
27017
|
+
}
|
|
27018
|
+
if (!tainted) continue;
|
|
27019
|
+
const op = /\bplugin\s*\.\s*Lookup\b/.test(line) ? "Lookup" : "Open";
|
|
27020
|
+
findings.push({
|
|
27021
|
+
id: `code_injection-${file}-${i2 + 1}-go-plugin-${op.toLowerCase()}`,
|
|
27022
|
+
pass: "language-sources",
|
|
27023
|
+
category: "security",
|
|
27024
|
+
rule_id: "code_injection",
|
|
27025
|
+
cwe: "CWE-94",
|
|
27026
|
+
severity: "critical",
|
|
27027
|
+
level: "error",
|
|
27028
|
+
message: `Code injection: ${sinkLabel(op)} called with a path/symbol derived from an *http.Request without an allow-list. Loading a plugin runs its init() and exposes arbitrary exported symbols. Restrict the path to a trusted directory or use a fixed allow-list.`,
|
|
27029
|
+
file,
|
|
27030
|
+
line: i2 + 1,
|
|
27031
|
+
snippet: line.trim()
|
|
27032
|
+
});
|
|
27033
|
+
}
|
|
27034
|
+
}
|
|
27035
|
+
return findings;
|
|
27036
|
+
}
|
|
27037
|
+
function findJsIndirectEvalCodeInjectionFindings(code, file) {
|
|
27038
|
+
const findings = [];
|
|
27039
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27040
|
+
if (!/\beval\b/.test(code)) return findings;
|
|
27041
|
+
const lines = code.split("\n");
|
|
27042
|
+
const reqExtractRe = /\breq(?:uest)?\s*\.\s*(?:body|query|params|headers|cookies)\b/;
|
|
27043
|
+
const aliasRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:globalThis\s*\.\s*eval|global\s*\.\s*eval|window\s*\.\s*eval|self\s*\.\s*eval|eval)\s*;?\s*$/;
|
|
27044
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
27045
|
+
for (const line of lines) {
|
|
27046
|
+
const m = line.match(aliasRe);
|
|
27047
|
+
if (m) aliases.add(m[1]);
|
|
27048
|
+
}
|
|
27049
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27050
|
+
const assignRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/;
|
|
27051
|
+
const reassignRe = /^\s*(\w+)\s*=\s*(.+?);?\s*$/;
|
|
27052
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27053
|
+
const before = taintedVars.size;
|
|
27054
|
+
for (const line of lines) {
|
|
27055
|
+
const m = line.match(assignRe) || line.match(reassignRe);
|
|
27056
|
+
if (!m) continue;
|
|
27057
|
+
const lhs = m[1];
|
|
27058
|
+
const rhs = m[2];
|
|
27059
|
+
if (taintedVars.has(lhs)) continue;
|
|
27060
|
+
if (lhs === "const" || lhs === "let" || lhs === "var") continue;
|
|
27061
|
+
if (reqExtractRe.test(rhs)) {
|
|
27062
|
+
taintedVars.add(lhs);
|
|
27063
|
+
continue;
|
|
27064
|
+
}
|
|
27065
|
+
for (const v of taintedVars) {
|
|
27066
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27067
|
+
if (re.test(rhs)) {
|
|
27068
|
+
taintedVars.add(lhs);
|
|
27069
|
+
break;
|
|
27070
|
+
}
|
|
27071
|
+
}
|
|
27072
|
+
}
|
|
27073
|
+
if (taintedVars.size === before) break;
|
|
27074
|
+
}
|
|
27075
|
+
const indirectCommaRe = /\(\s*0\s*,\s*eval\s*\)\s*\(\s*([^)]*)\s*\)/;
|
|
27076
|
+
const indirectMemberRe = /\b(?:globalThis|global|window|self)\s*\.\s*eval\s*\(\s*([^)]*)\s*\)/;
|
|
27077
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27078
|
+
const line = lines[i2];
|
|
27079
|
+
const trimmed = line.trim();
|
|
27080
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
27081
|
+
if (aliasRe.test(line)) continue;
|
|
27082
|
+
let arg = null;
|
|
27083
|
+
let formLabel = "";
|
|
27084
|
+
let m = trimmed.match(indirectCommaRe);
|
|
27085
|
+
if (m) {
|
|
27086
|
+
arg = m[1].trim();
|
|
27087
|
+
formLabel = "(0, eval)(...) indirect eval";
|
|
27088
|
+
}
|
|
27089
|
+
if (!arg) {
|
|
27090
|
+
m = trimmed.match(indirectMemberRe);
|
|
27091
|
+
if (m) {
|
|
27092
|
+
arg = m[1].trim();
|
|
27093
|
+
formLabel = "globalThis.eval / window.eval / self.eval indirect eval";
|
|
27094
|
+
}
|
|
27095
|
+
}
|
|
27096
|
+
if (!arg && aliases.size > 0) {
|
|
27097
|
+
for (const a of aliases) {
|
|
27098
|
+
const aliasCallRe = new RegExp(`\\b${a}\\s*\\(\\s*([^)]*)\\s*\\)`);
|
|
27099
|
+
const mm = trimmed.match(aliasCallRe);
|
|
27100
|
+
if (mm) {
|
|
27101
|
+
arg = mm[1].trim();
|
|
27102
|
+
formLabel = `aliased eval reference \`${a}(...)\``;
|
|
27103
|
+
break;
|
|
27104
|
+
}
|
|
27105
|
+
}
|
|
27106
|
+
}
|
|
27107
|
+
if (arg === null) continue;
|
|
27108
|
+
if (arg.length === 0) continue;
|
|
27109
|
+
if (/^['"`][^'"`]*['"`]$/.test(arg)) continue;
|
|
27110
|
+
let tainted = false;
|
|
27111
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27112
|
+
else {
|
|
27113
|
+
for (const v of taintedVars) {
|
|
27114
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27115
|
+
if (re.test(arg)) {
|
|
27116
|
+
tainted = true;
|
|
27117
|
+
break;
|
|
27118
|
+
}
|
|
27119
|
+
}
|
|
27120
|
+
}
|
|
27121
|
+
if (!tainted) continue;
|
|
27122
|
+
findings.push({
|
|
27123
|
+
id: `code_injection-${file}-${i2 + 1}-js-indirect-eval`,
|
|
27124
|
+
pass: "language-sources",
|
|
27125
|
+
category: "security",
|
|
27126
|
+
rule_id: "code_injection",
|
|
27127
|
+
cwe: "CWE-94",
|
|
27128
|
+
severity: "critical",
|
|
27129
|
+
level: "error",
|
|
27130
|
+
message: `Code injection: ${formLabel} called with a value derived from an HTTP request body/query/headers. Indirect eval forms still execute arbitrary code in the global scope. Remove the eval and parse the input with a safe data parser instead.`,
|
|
27131
|
+
file,
|
|
27132
|
+
line: i2 + 1,
|
|
27133
|
+
snippet: trimmed
|
|
27134
|
+
});
|
|
27135
|
+
}
|
|
27136
|
+
return findings;
|
|
27137
|
+
}
|
|
27138
|
+
function findPythonInteractiveInterpreterCodeInjectionFindings(code, file) {
|
|
27139
|
+
const findings = [];
|
|
27140
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27141
|
+
if (!/^\s*import\s+code\b/m.test(code)) return findings;
|
|
27142
|
+
if (!/\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole|compile_command)\b/.test(
|
|
27143
|
+
code
|
|
27144
|
+
)) {
|
|
27145
|
+
return findings;
|
|
27146
|
+
}
|
|
27147
|
+
const lines = code.split("\n");
|
|
27148
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|files|json|cookies|headers|get_data|get_json)\b/;
|
|
27149
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27150
|
+
const assignRe = /^\s*(\w+)\s*=\s*(.+?)\s*(?:#.*)?$/;
|
|
27151
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27152
|
+
const before = taintedVars.size;
|
|
27153
|
+
for (const line of lines) {
|
|
27154
|
+
const m = line.match(assignRe);
|
|
27155
|
+
if (!m) continue;
|
|
27156
|
+
const lhs = m[1];
|
|
27157
|
+
const rhs = m[2];
|
|
27158
|
+
if (taintedVars.has(lhs)) continue;
|
|
27159
|
+
if (reqExtractRe.test(rhs)) {
|
|
27160
|
+
taintedVars.add(lhs);
|
|
27161
|
+
continue;
|
|
27162
|
+
}
|
|
27163
|
+
for (const v of taintedVars) {
|
|
27164
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27165
|
+
if (re.test(rhs)) {
|
|
27166
|
+
taintedVars.add(lhs);
|
|
27167
|
+
break;
|
|
27168
|
+
}
|
|
27169
|
+
}
|
|
27170
|
+
}
|
|
27171
|
+
if (taintedVars.size === before) break;
|
|
27172
|
+
}
|
|
27173
|
+
const callRe = /\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole)\s*\([^)]*\)\s*\.\s*(runsource|runcode|push|interact)\s*\(\s*([^),]+)/;
|
|
27174
|
+
const compileRe = /\bcode\s*\.\s*compile_command\s*\(\s*([^),]+)/;
|
|
27175
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27176
|
+
const line = lines[i2];
|
|
27177
|
+
const trimmed = line.trim();
|
|
27178
|
+
let arg = null;
|
|
27179
|
+
let formLabel = "";
|
|
27180
|
+
const m1 = trimmed.match(callRe);
|
|
27181
|
+
if (m1) {
|
|
27182
|
+
arg = m1[2].trim();
|
|
27183
|
+
formLabel = `code.${/Interpreter/.test(trimmed) ? "InteractiveInterpreter" : "InteractiveConsole"}().${m1[1]}`;
|
|
27184
|
+
}
|
|
27185
|
+
if (!arg) {
|
|
27186
|
+
const m2 = trimmed.match(compileRe);
|
|
27187
|
+
if (m2) {
|
|
27188
|
+
arg = m2[1].trim();
|
|
27189
|
+
formLabel = "code.compile_command";
|
|
27190
|
+
}
|
|
27191
|
+
}
|
|
27192
|
+
if (arg === null) continue;
|
|
27193
|
+
if (arg.length === 0) continue;
|
|
27194
|
+
if (/^['"][^'"]*['"]$/.test(arg)) continue;
|
|
27195
|
+
let tainted = false;
|
|
27196
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27197
|
+
else {
|
|
27198
|
+
for (const v of taintedVars) {
|
|
27199
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27200
|
+
if (re.test(arg)) {
|
|
27201
|
+
tainted = true;
|
|
27202
|
+
break;
|
|
27203
|
+
}
|
|
27204
|
+
}
|
|
27205
|
+
}
|
|
27206
|
+
if (!tainted) continue;
|
|
27207
|
+
findings.push({
|
|
27208
|
+
id: `code_injection-${file}-${i2 + 1}-py-interactive-interpreter`,
|
|
27209
|
+
pass: "language-sources",
|
|
27210
|
+
category: "security",
|
|
27211
|
+
rule_id: "code_injection",
|
|
27212
|
+
cwe: "CWE-94",
|
|
27213
|
+
severity: "critical",
|
|
27214
|
+
level: "error",
|
|
27215
|
+
message: `Code injection: ${formLabel}(...) called with a value derived from a Flask request extractor. The Python \`code\` module compiles and executes arbitrary source. Remove the call and validate input against a fixed allow-list instead.`,
|
|
27216
|
+
file,
|
|
27217
|
+
line: i2 + 1,
|
|
27218
|
+
snippet: trimmed
|
|
27219
|
+
});
|
|
27220
|
+
}
|
|
27221
|
+
return findings;
|
|
27222
|
+
}
|
|
27223
|
+
function findRustEvalCrateCodeInjectionFindings(code, file) {
|
|
27224
|
+
const findings = [];
|
|
27225
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27226
|
+
if (!/\b(?:evalexpr\s*::\s*eval|libloading\s*::\s*Library\s*::\s*new|\.\s*load\s*\([^)]*\)\s*\.\s*(?:exec|eval|call))/.test(
|
|
27227
|
+
code
|
|
27228
|
+
)) {
|
|
27229
|
+
return findings;
|
|
27230
|
+
}
|
|
27231
|
+
const lines = code.split("\n");
|
|
27232
|
+
const extractorTypeRe = /:\s*(?:String|Bytes|bytes::Bytes|axum::body::Bytes|web::Query\b|web::Path\b|web::Form\b|web::Json\b|HttpRequest\b|actix_web::HttpRequest\b)/;
|
|
27233
|
+
const fns = [];
|
|
27234
|
+
let cur = null;
|
|
27235
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27236
|
+
const t = lines[i2];
|
|
27237
|
+
if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
|
|
27238
|
+
if (cur) {
|
|
27239
|
+
cur.end = i2 - 1;
|
|
27240
|
+
fns.push(cur);
|
|
27241
|
+
}
|
|
27242
|
+
cur = { start: i2, end: lines.length - 1, tainted: /* @__PURE__ */ new Set() };
|
|
27243
|
+
const headerJoined = (() => {
|
|
27244
|
+
let j = i2;
|
|
27245
|
+
let s = "";
|
|
27246
|
+
while (j < lines.length && !/\{\s*$/.test(s)) {
|
|
27247
|
+
s += lines[j];
|
|
27248
|
+
if (/\{\s*$/.test(lines[j])) break;
|
|
27249
|
+
j++;
|
|
27250
|
+
if (j - i2 > 12) break;
|
|
27251
|
+
}
|
|
27252
|
+
return s;
|
|
27253
|
+
})();
|
|
27254
|
+
const open = headerJoined.indexOf("(");
|
|
27255
|
+
const close = headerJoined.lastIndexOf(")");
|
|
27256
|
+
if (open !== -1 && close > open) {
|
|
27257
|
+
const params = headerJoined.substring(open + 1, close);
|
|
27258
|
+
let depth = 0;
|
|
27259
|
+
let buf = "";
|
|
27260
|
+
const parts2 = [];
|
|
27261
|
+
for (const ch of params) {
|
|
27262
|
+
if (ch === "<" || ch === "(") depth++;
|
|
27263
|
+
else if (ch === ">" || ch === ")") depth--;
|
|
27264
|
+
if (ch === "," && depth === 0) {
|
|
27265
|
+
parts2.push(buf);
|
|
27266
|
+
buf = "";
|
|
27267
|
+
continue;
|
|
27268
|
+
}
|
|
27269
|
+
buf += ch;
|
|
27270
|
+
}
|
|
27271
|
+
if (buf.trim().length > 0) parts2.push(buf);
|
|
27272
|
+
for (const p of parts2) {
|
|
27273
|
+
const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
|
|
27274
|
+
if (!pm) continue;
|
|
27275
|
+
if (extractorTypeRe.test(p)) cur.tainted.add(pm[1]);
|
|
27276
|
+
}
|
|
27277
|
+
}
|
|
27278
|
+
}
|
|
27279
|
+
}
|
|
27280
|
+
if (cur) fns.push(cur);
|
|
27281
|
+
for (const fn of fns) {
|
|
27282
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27283
|
+
const before = fn.tainted.size;
|
|
27284
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27285
|
+
const t = lines[i2].trim();
|
|
27286
|
+
const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
|
|
27287
|
+
if (!m) continue;
|
|
27288
|
+
const lhs = m[1];
|
|
27289
|
+
const rhs = m[2];
|
|
27290
|
+
if (fn.tainted.has(lhs)) continue;
|
|
27291
|
+
for (const v of fn.tainted) {
|
|
27292
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27293
|
+
if (re.test(rhs)) {
|
|
27294
|
+
fn.tainted.add(lhs);
|
|
27295
|
+
break;
|
|
27296
|
+
}
|
|
27297
|
+
}
|
|
27298
|
+
}
|
|
27299
|
+
if (fn.tainted.size === before) break;
|
|
27300
|
+
}
|
|
27301
|
+
}
|
|
27302
|
+
const evalExprRe = /\bevalexpr\s*::\s*eval(?:_with_context|_boolean|_int|_float|_string|_tuple|_empty)?\s*\(\s*([^)]+)\s*\)/;
|
|
27303
|
+
const libloadingRe = /\blibloading\s*::\s*Library\s*::\s*new\s*\(\s*([^)]+)\s*\)/;
|
|
27304
|
+
const luaLoadRe = /\.\s*load\s*\(\s*([^)]+)\s*\)\s*\.\s*(?:exec|eval|call)\b/;
|
|
27305
|
+
for (const fn of fns) {
|
|
27306
|
+
if (fn.tainted.size === 0) continue;
|
|
27307
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27308
|
+
const line = lines[i2];
|
|
27309
|
+
const trimmed = line.trim();
|
|
27310
|
+
let arg = null;
|
|
27311
|
+
let formLabel = "";
|
|
27312
|
+
let m = trimmed.match(evalExprRe);
|
|
27313
|
+
if (m) {
|
|
27314
|
+
arg = m[1].trim();
|
|
27315
|
+
formLabel = "evalexpr::eval";
|
|
27316
|
+
}
|
|
27317
|
+
if (!arg) {
|
|
27318
|
+
m = trimmed.match(libloadingRe);
|
|
27319
|
+
if (m) {
|
|
27320
|
+
arg = m[1].trim();
|
|
27321
|
+
formLabel = "libloading::Library::new";
|
|
27322
|
+
}
|
|
27323
|
+
}
|
|
27324
|
+
if (!arg) {
|
|
27325
|
+
m = trimmed.match(luaLoadRe);
|
|
27326
|
+
if (m) {
|
|
27327
|
+
arg = m[1].trim();
|
|
27328
|
+
formLabel = "mlua/rlua Lua::load().{exec|eval|call}";
|
|
27329
|
+
}
|
|
27330
|
+
}
|
|
27331
|
+
if (arg === null) continue;
|
|
27332
|
+
if (arg.length === 0) continue;
|
|
27333
|
+
let unwrapped = arg.replace(/^&\s*/, "").trim();
|
|
27334
|
+
if (/^"[^"]*"$/.test(unwrapped)) continue;
|
|
27335
|
+
let tainted = false;
|
|
27336
|
+
for (const v of fn.tainted) {
|
|
27337
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27338
|
+
if (re.test(unwrapped)) {
|
|
27339
|
+
tainted = true;
|
|
27340
|
+
break;
|
|
27341
|
+
}
|
|
27342
|
+
}
|
|
27343
|
+
if (!tainted) continue;
|
|
27344
|
+
findings.push({
|
|
27345
|
+
id: `code_injection-${file}-${i2 + 1}-rust-eval-crate`,
|
|
27346
|
+
pass: "language-sources",
|
|
27347
|
+
category: "security",
|
|
27348
|
+
rule_id: "code_injection",
|
|
27349
|
+
cwe: "CWE-94",
|
|
27350
|
+
severity: "critical",
|
|
27351
|
+
level: "error",
|
|
27352
|
+
message: `Code injection: ${formLabel}(...) called with a value derived from an HTTP request extractor (body / Query / Path / Form / Json / HttpRequest). The expression / library / Lua chunk is executed as code. Remove the dynamic-eval path or restrict input to a fixed allow-list.`,
|
|
27353
|
+
file,
|
|
27354
|
+
line: i2 + 1,
|
|
27355
|
+
snippet: trimmed
|
|
27356
|
+
});
|
|
27357
|
+
}
|
|
27358
|
+
}
|
|
27359
|
+
return findings;
|
|
27360
|
+
}
|
|
26619
27361
|
|
|
26620
27362
|
// src/analysis/passes/sink-filter-pass.ts
|
|
26621
27363
|
var JS_XSS_SANITIZERS = [
|
|
@@ -27950,13 +28692,13 @@ var TaintPropagationPass = class {
|
|
|
27950
28692
|
}));
|
|
27951
28693
|
const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
|
|
27952
28694
|
for (const f of arrayFlows) {
|
|
27953
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28695
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27954
28696
|
flows.push(f);
|
|
27955
28697
|
}
|
|
27956
28698
|
}
|
|
27957
28699
|
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
|
|
27958
28700
|
for (const f of collectionFlows) {
|
|
27959
|
-
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
|
|
28701
|
+
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) continue;
|
|
27960
28702
|
const flowForCheck = {
|
|
27961
28703
|
source: { line: f.source_line },
|
|
27962
28704
|
sink: { line: f.sink_line },
|
|
@@ -27975,7 +28717,7 @@ var TaintPropagationPass = class {
|
|
|
27975
28717
|
}
|
|
27976
28718
|
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
27977
28719
|
for (const f of paramFlows) {
|
|
27978
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28720
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27979
28721
|
flows.push(f);
|
|
27980
28722
|
}
|
|
27981
28723
|
}
|
|
@@ -28365,7 +29107,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
28365
29107
|
}
|
|
28366
29108
|
const paramSource = methodParamSources.get(arg.variable);
|
|
28367
29109
|
if (paramSource) {
|
|
28368
|
-
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
29110
|
+
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
28369
29111
|
if (!exists) {
|
|
28370
29112
|
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
28371
29113
|
continue;
|