circle-ir 3.132.0 → 3.134.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/passes/language-sources-pass.d.ts +67 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +882 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +686 -0
- package/package.json +1 -1
|
@@ -23672,6 +23672,18 @@ var LanguageSourcesPass = class {
|
|
|
23672
23672
|
)) {
|
|
23673
23673
|
ctx.addFinding(finding);
|
|
23674
23674
|
}
|
|
23675
|
+
for (const finding of findGoPluginOpenCodeInjectionFindings(
|
|
23676
|
+
code,
|
|
23677
|
+
graph.ir.meta.file
|
|
23678
|
+
)) {
|
|
23679
|
+
ctx.addFinding(finding);
|
|
23680
|
+
}
|
|
23681
|
+
for (const finding of findGoMongoNosqlInjectionFindings(
|
|
23682
|
+
code,
|
|
23683
|
+
graph.ir.meta.file
|
|
23684
|
+
)) {
|
|
23685
|
+
ctx.addFinding(finding);
|
|
23686
|
+
}
|
|
23675
23687
|
}
|
|
23676
23688
|
if (language === "python") {
|
|
23677
23689
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -23696,6 +23708,18 @@ var LanguageSourcesPass = class {
|
|
|
23696
23708
|
)) {
|
|
23697
23709
|
ctx.addFinding(finding);
|
|
23698
23710
|
}
|
|
23711
|
+
for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(
|
|
23712
|
+
code,
|
|
23713
|
+
graph.ir.meta.file
|
|
23714
|
+
)) {
|
|
23715
|
+
ctx.addFinding(finding);
|
|
23716
|
+
}
|
|
23717
|
+
for (const finding of findPythonMongoengineWhereNosqlInjectionFindings(
|
|
23718
|
+
code,
|
|
23719
|
+
graph.ir.meta.file
|
|
23720
|
+
)) {
|
|
23721
|
+
ctx.addFinding(finding);
|
|
23722
|
+
}
|
|
23699
23723
|
}
|
|
23700
23724
|
if (language === "rust") {
|
|
23701
23725
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -23723,6 +23747,12 @@ var LanguageSourcesPass = class {
|
|
|
23723
23747
|
)) {
|
|
23724
23748
|
ctx.addFinding(finding);
|
|
23725
23749
|
}
|
|
23750
|
+
for (const finding of findRustEvalCrateCodeInjectionFindings(
|
|
23751
|
+
code,
|
|
23752
|
+
graph.ir.meta.file
|
|
23753
|
+
)) {
|
|
23754
|
+
ctx.addFinding(finding);
|
|
23755
|
+
}
|
|
23726
23756
|
}
|
|
23727
23757
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
23728
23758
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -23747,6 +23777,12 @@ var LanguageSourcesPass = class {
|
|
|
23747
23777
|
)) {
|
|
23748
23778
|
ctx.addFinding(finding);
|
|
23749
23779
|
}
|
|
23780
|
+
for (const finding of findJsIndirectEvalCodeInjectionFindings(
|
|
23781
|
+
code,
|
|
23782
|
+
graph.ir.meta.file
|
|
23783
|
+
)) {
|
|
23784
|
+
ctx.addFinding(finding);
|
|
23785
|
+
}
|
|
23750
23786
|
}
|
|
23751
23787
|
if (language === "java") {
|
|
23752
23788
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -23761,6 +23797,12 @@ var LanguageSourcesPass = class {
|
|
|
23761
23797
|
for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
|
|
23762
23798
|
ctx.addFinding(finding);
|
|
23763
23799
|
}
|
|
23800
|
+
for (const finding of findJavaMongoNosqlInjectionFindings(
|
|
23801
|
+
code,
|
|
23802
|
+
graph.ir.meta.file
|
|
23803
|
+
)) {
|
|
23804
|
+
ctx.addFinding(finding);
|
|
23805
|
+
}
|
|
23764
23806
|
}
|
|
23765
23807
|
if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
|
|
23766
23808
|
const exfilFindings = findExternalSecretExfiltrationFindings(
|
|
@@ -26920,6 +26962,650 @@ function findJsDomOpenRedirectFindings(code, file) {
|
|
|
26920
26962
|
}
|
|
26921
26963
|
return out2;
|
|
26922
26964
|
}
|
|
26965
|
+
function findGoPluginOpenCodeInjectionFindings(code, file) {
|
|
26966
|
+
const findings = [];
|
|
26967
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
26968
|
+
if (!/\bplugin\s*\.\s*(?:Open|Lookup)\s*\(/.test(code)) return findings;
|
|
26969
|
+
const lines = code.split("\n");
|
|
26970
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\.Query\(\)\.Get|Header\.Get|Cookie)\s*\(/;
|
|
26971
|
+
const httpReqParamRe = /\*\s*http\.Request\b/;
|
|
26972
|
+
const callRe = /\bplugin\s*\.\s*(?:Open|Lookup)\s*\(\s*([^)]*)\s*\)/;
|
|
26973
|
+
const sinkLabel = (op) => op === "Lookup" ? "Go plugin.Lookup" : "Go plugin.Open";
|
|
26974
|
+
const funcs = [];
|
|
26975
|
+
let cur = null;
|
|
26976
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26977
|
+
const t = lines[i2].trim();
|
|
26978
|
+
if (/^func\b/.test(t)) {
|
|
26979
|
+
if (cur) {
|
|
26980
|
+
cur.end = i2 - 1;
|
|
26981
|
+
funcs.push(cur);
|
|
26982
|
+
}
|
|
26983
|
+
cur = { start: i2, end: lines.length - 1 };
|
|
26984
|
+
}
|
|
26985
|
+
}
|
|
26986
|
+
if (cur) funcs.push(cur);
|
|
26987
|
+
for (const fn of funcs) {
|
|
26988
|
+
const header = lines[fn.start];
|
|
26989
|
+
if (!httpReqParamRe.test(header)) continue;
|
|
26990
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
26991
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
26992
|
+
const before = taintedVars.size;
|
|
26993
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
26994
|
+
const line = lines[i2];
|
|
26995
|
+
const trimmed = line.trim();
|
|
26996
|
+
const assignMatch = trimmed.match(
|
|
26997
|
+
/^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/
|
|
26998
|
+
);
|
|
26999
|
+
if (!assignMatch) continue;
|
|
27000
|
+
const lhs = assignMatch[1];
|
|
27001
|
+
const rhs = assignMatch[2];
|
|
27002
|
+
if (taintedVars.has(lhs)) continue;
|
|
27003
|
+
if (reqExtractRe.test(rhs)) {
|
|
27004
|
+
taintedVars.add(lhs);
|
|
27005
|
+
continue;
|
|
27006
|
+
}
|
|
27007
|
+
for (const v of taintedVars) {
|
|
27008
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27009
|
+
if (re.test(rhs)) {
|
|
27010
|
+
taintedVars.add(lhs);
|
|
27011
|
+
break;
|
|
27012
|
+
}
|
|
27013
|
+
}
|
|
27014
|
+
}
|
|
27015
|
+
if (taintedVars.size === before) break;
|
|
27016
|
+
}
|
|
27017
|
+
if (taintedVars.size === 0) continue;
|
|
27018
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27019
|
+
const line = lines[i2];
|
|
27020
|
+
const m = line.match(callRe);
|
|
27021
|
+
if (!m) continue;
|
|
27022
|
+
const arg = m[1].trim();
|
|
27023
|
+
if (arg.length === 0) continue;
|
|
27024
|
+
if (/^"[^"]*"$/.test(arg)) continue;
|
|
27025
|
+
let tainted = false;
|
|
27026
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27027
|
+
else {
|
|
27028
|
+
for (const v of taintedVars) {
|
|
27029
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27030
|
+
if (re.test(arg)) {
|
|
27031
|
+
tainted = true;
|
|
27032
|
+
break;
|
|
27033
|
+
}
|
|
27034
|
+
}
|
|
27035
|
+
}
|
|
27036
|
+
if (!tainted) continue;
|
|
27037
|
+
const op = /\bplugin\s*\.\s*Lookup\b/.test(line) ? "Lookup" : "Open";
|
|
27038
|
+
findings.push({
|
|
27039
|
+
id: `code_injection-${file}-${i2 + 1}-go-plugin-${op.toLowerCase()}`,
|
|
27040
|
+
pass: "language-sources",
|
|
27041
|
+
category: "security",
|
|
27042
|
+
rule_id: "code_injection",
|
|
27043
|
+
cwe: "CWE-94",
|
|
27044
|
+
severity: "critical",
|
|
27045
|
+
level: "error",
|
|
27046
|
+
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.`,
|
|
27047
|
+
file,
|
|
27048
|
+
line: i2 + 1,
|
|
27049
|
+
snippet: line.trim()
|
|
27050
|
+
});
|
|
27051
|
+
}
|
|
27052
|
+
}
|
|
27053
|
+
return findings;
|
|
27054
|
+
}
|
|
27055
|
+
function findJsIndirectEvalCodeInjectionFindings(code, file) {
|
|
27056
|
+
const findings = [];
|
|
27057
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27058
|
+
if (!/\beval\b/.test(code)) return findings;
|
|
27059
|
+
const lines = code.split("\n");
|
|
27060
|
+
const reqExtractRe = /\breq(?:uest)?\s*\.\s*(?:body|query|params|headers|cookies)\b/;
|
|
27061
|
+
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*$/;
|
|
27062
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
27063
|
+
for (const line of lines) {
|
|
27064
|
+
const m = line.match(aliasRe);
|
|
27065
|
+
if (m) aliases.add(m[1]);
|
|
27066
|
+
}
|
|
27067
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27068
|
+
const assignRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/;
|
|
27069
|
+
const reassignRe = /^\s*(\w+)\s*=\s*(.+?);?\s*$/;
|
|
27070
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27071
|
+
const before = taintedVars.size;
|
|
27072
|
+
for (const line of lines) {
|
|
27073
|
+
const m = line.match(assignRe) || line.match(reassignRe);
|
|
27074
|
+
if (!m) continue;
|
|
27075
|
+
const lhs = m[1];
|
|
27076
|
+
const rhs = m[2];
|
|
27077
|
+
if (taintedVars.has(lhs)) continue;
|
|
27078
|
+
if (lhs === "const" || lhs === "let" || lhs === "var") continue;
|
|
27079
|
+
if (reqExtractRe.test(rhs)) {
|
|
27080
|
+
taintedVars.add(lhs);
|
|
27081
|
+
continue;
|
|
27082
|
+
}
|
|
27083
|
+
for (const v of taintedVars) {
|
|
27084
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27085
|
+
if (re.test(rhs)) {
|
|
27086
|
+
taintedVars.add(lhs);
|
|
27087
|
+
break;
|
|
27088
|
+
}
|
|
27089
|
+
}
|
|
27090
|
+
}
|
|
27091
|
+
if (taintedVars.size === before) break;
|
|
27092
|
+
}
|
|
27093
|
+
const indirectCommaRe = /\(\s*0\s*,\s*eval\s*\)\s*\(\s*([^)]*)\s*\)/;
|
|
27094
|
+
const indirectMemberRe = /\b(?:globalThis|global|window|self)\s*\.\s*eval\s*\(\s*([^)]*)\s*\)/;
|
|
27095
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27096
|
+
const line = lines[i2];
|
|
27097
|
+
const trimmed = line.trim();
|
|
27098
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
27099
|
+
if (aliasRe.test(line)) continue;
|
|
27100
|
+
let arg = null;
|
|
27101
|
+
let formLabel = "";
|
|
27102
|
+
let m = trimmed.match(indirectCommaRe);
|
|
27103
|
+
if (m) {
|
|
27104
|
+
arg = m[1].trim();
|
|
27105
|
+
formLabel = "(0, eval)(...) indirect eval";
|
|
27106
|
+
}
|
|
27107
|
+
if (!arg) {
|
|
27108
|
+
m = trimmed.match(indirectMemberRe);
|
|
27109
|
+
if (m) {
|
|
27110
|
+
arg = m[1].trim();
|
|
27111
|
+
formLabel = "globalThis.eval / window.eval / self.eval indirect eval";
|
|
27112
|
+
}
|
|
27113
|
+
}
|
|
27114
|
+
if (!arg && aliases.size > 0) {
|
|
27115
|
+
for (const a of aliases) {
|
|
27116
|
+
const aliasCallRe = new RegExp(`\\b${a}\\s*\\(\\s*([^)]*)\\s*\\)`);
|
|
27117
|
+
const mm = trimmed.match(aliasCallRe);
|
|
27118
|
+
if (mm) {
|
|
27119
|
+
arg = mm[1].trim();
|
|
27120
|
+
formLabel = `aliased eval reference \`${a}(...)\``;
|
|
27121
|
+
break;
|
|
27122
|
+
}
|
|
27123
|
+
}
|
|
27124
|
+
}
|
|
27125
|
+
if (arg === null) continue;
|
|
27126
|
+
if (arg.length === 0) continue;
|
|
27127
|
+
if (/^['"`][^'"`]*['"`]$/.test(arg)) continue;
|
|
27128
|
+
let tainted = false;
|
|
27129
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27130
|
+
else {
|
|
27131
|
+
for (const v of taintedVars) {
|
|
27132
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27133
|
+
if (re.test(arg)) {
|
|
27134
|
+
tainted = true;
|
|
27135
|
+
break;
|
|
27136
|
+
}
|
|
27137
|
+
}
|
|
27138
|
+
}
|
|
27139
|
+
if (!tainted) continue;
|
|
27140
|
+
findings.push({
|
|
27141
|
+
id: `code_injection-${file}-${i2 + 1}-js-indirect-eval`,
|
|
27142
|
+
pass: "language-sources",
|
|
27143
|
+
category: "security",
|
|
27144
|
+
rule_id: "code_injection",
|
|
27145
|
+
cwe: "CWE-94",
|
|
27146
|
+
severity: "critical",
|
|
27147
|
+
level: "error",
|
|
27148
|
+
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.`,
|
|
27149
|
+
file,
|
|
27150
|
+
line: i2 + 1,
|
|
27151
|
+
snippet: trimmed
|
|
27152
|
+
});
|
|
27153
|
+
}
|
|
27154
|
+
return findings;
|
|
27155
|
+
}
|
|
27156
|
+
function findPythonInteractiveInterpreterCodeInjectionFindings(code, file) {
|
|
27157
|
+
const findings = [];
|
|
27158
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27159
|
+
if (!/^\s*import\s+code\b/m.test(code)) return findings;
|
|
27160
|
+
if (!/\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole|compile_command)\b/.test(
|
|
27161
|
+
code
|
|
27162
|
+
)) {
|
|
27163
|
+
return findings;
|
|
27164
|
+
}
|
|
27165
|
+
const lines = code.split("\n");
|
|
27166
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|files|json|cookies|headers|get_data|get_json)\b/;
|
|
27167
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27168
|
+
const assignRe = /^\s*(\w+)\s*=\s*(.+?)\s*(?:#.*)?$/;
|
|
27169
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27170
|
+
const before = taintedVars.size;
|
|
27171
|
+
for (const line of lines) {
|
|
27172
|
+
const m = line.match(assignRe);
|
|
27173
|
+
if (!m) continue;
|
|
27174
|
+
const lhs = m[1];
|
|
27175
|
+
const rhs = m[2];
|
|
27176
|
+
if (taintedVars.has(lhs)) continue;
|
|
27177
|
+
if (reqExtractRe.test(rhs)) {
|
|
27178
|
+
taintedVars.add(lhs);
|
|
27179
|
+
continue;
|
|
27180
|
+
}
|
|
27181
|
+
for (const v of taintedVars) {
|
|
27182
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27183
|
+
if (re.test(rhs)) {
|
|
27184
|
+
taintedVars.add(lhs);
|
|
27185
|
+
break;
|
|
27186
|
+
}
|
|
27187
|
+
}
|
|
27188
|
+
}
|
|
27189
|
+
if (taintedVars.size === before) break;
|
|
27190
|
+
}
|
|
27191
|
+
const callRe = /\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole)\s*\([^)]*\)\s*\.\s*(runsource|runcode|push|interact)\s*\(\s*([^),]+)/;
|
|
27192
|
+
const compileRe = /\bcode\s*\.\s*compile_command\s*\(\s*([^),]+)/;
|
|
27193
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27194
|
+
const line = lines[i2];
|
|
27195
|
+
const trimmed = line.trim();
|
|
27196
|
+
let arg = null;
|
|
27197
|
+
let formLabel = "";
|
|
27198
|
+
const m1 = trimmed.match(callRe);
|
|
27199
|
+
if (m1) {
|
|
27200
|
+
arg = m1[2].trim();
|
|
27201
|
+
formLabel = `code.${/Interpreter/.test(trimmed) ? "InteractiveInterpreter" : "InteractiveConsole"}().${m1[1]}`;
|
|
27202
|
+
}
|
|
27203
|
+
if (!arg) {
|
|
27204
|
+
const m2 = trimmed.match(compileRe);
|
|
27205
|
+
if (m2) {
|
|
27206
|
+
arg = m2[1].trim();
|
|
27207
|
+
formLabel = "code.compile_command";
|
|
27208
|
+
}
|
|
27209
|
+
}
|
|
27210
|
+
if (arg === null) continue;
|
|
27211
|
+
if (arg.length === 0) continue;
|
|
27212
|
+
if (/^['"][^'"]*['"]$/.test(arg)) continue;
|
|
27213
|
+
let tainted = false;
|
|
27214
|
+
if (reqExtractRe.test(arg)) tainted = true;
|
|
27215
|
+
else {
|
|
27216
|
+
for (const v of taintedVars) {
|
|
27217
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27218
|
+
if (re.test(arg)) {
|
|
27219
|
+
tainted = true;
|
|
27220
|
+
break;
|
|
27221
|
+
}
|
|
27222
|
+
}
|
|
27223
|
+
}
|
|
27224
|
+
if (!tainted) continue;
|
|
27225
|
+
findings.push({
|
|
27226
|
+
id: `code_injection-${file}-${i2 + 1}-py-interactive-interpreter`,
|
|
27227
|
+
pass: "language-sources",
|
|
27228
|
+
category: "security",
|
|
27229
|
+
rule_id: "code_injection",
|
|
27230
|
+
cwe: "CWE-94",
|
|
27231
|
+
severity: "critical",
|
|
27232
|
+
level: "error",
|
|
27233
|
+
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.`,
|
|
27234
|
+
file,
|
|
27235
|
+
line: i2 + 1,
|
|
27236
|
+
snippet: trimmed
|
|
27237
|
+
});
|
|
27238
|
+
}
|
|
27239
|
+
return findings;
|
|
27240
|
+
}
|
|
27241
|
+
function findRustEvalCrateCodeInjectionFindings(code, file) {
|
|
27242
|
+
const findings = [];
|
|
27243
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27244
|
+
if (!/\b(?:evalexpr\s*::\s*eval|libloading\s*::\s*Library\s*::\s*new|\.\s*load\s*\([^)]*\)\s*\.\s*(?:exec|eval|call))/.test(
|
|
27245
|
+
code
|
|
27246
|
+
)) {
|
|
27247
|
+
return findings;
|
|
27248
|
+
}
|
|
27249
|
+
const lines = code.split("\n");
|
|
27250
|
+
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)/;
|
|
27251
|
+
const fns = [];
|
|
27252
|
+
let cur = null;
|
|
27253
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27254
|
+
const t = lines[i2];
|
|
27255
|
+
if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
|
|
27256
|
+
if (cur) {
|
|
27257
|
+
cur.end = i2 - 1;
|
|
27258
|
+
fns.push(cur);
|
|
27259
|
+
}
|
|
27260
|
+
cur = { start: i2, end: lines.length - 1, tainted: /* @__PURE__ */ new Set() };
|
|
27261
|
+
const headerJoined = (() => {
|
|
27262
|
+
let j = i2;
|
|
27263
|
+
let s = "";
|
|
27264
|
+
while (j < lines.length && !/\{\s*$/.test(s)) {
|
|
27265
|
+
s += lines[j];
|
|
27266
|
+
if (/\{\s*$/.test(lines[j])) break;
|
|
27267
|
+
j++;
|
|
27268
|
+
if (j - i2 > 12) break;
|
|
27269
|
+
}
|
|
27270
|
+
return s;
|
|
27271
|
+
})();
|
|
27272
|
+
const open = headerJoined.indexOf("(");
|
|
27273
|
+
const close = headerJoined.lastIndexOf(")");
|
|
27274
|
+
if (open !== -1 && close > open) {
|
|
27275
|
+
const params = headerJoined.substring(open + 1, close);
|
|
27276
|
+
let depth = 0;
|
|
27277
|
+
let buf = "";
|
|
27278
|
+
const parts2 = [];
|
|
27279
|
+
for (const ch of params) {
|
|
27280
|
+
if (ch === "<" || ch === "(") depth++;
|
|
27281
|
+
else if (ch === ">" || ch === ")") depth--;
|
|
27282
|
+
if (ch === "," && depth === 0) {
|
|
27283
|
+
parts2.push(buf);
|
|
27284
|
+
buf = "";
|
|
27285
|
+
continue;
|
|
27286
|
+
}
|
|
27287
|
+
buf += ch;
|
|
27288
|
+
}
|
|
27289
|
+
if (buf.trim().length > 0) parts2.push(buf);
|
|
27290
|
+
for (const p of parts2) {
|
|
27291
|
+
const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
|
|
27292
|
+
if (!pm) continue;
|
|
27293
|
+
if (extractorTypeRe.test(p)) cur.tainted.add(pm[1]);
|
|
27294
|
+
}
|
|
27295
|
+
}
|
|
27296
|
+
}
|
|
27297
|
+
}
|
|
27298
|
+
if (cur) fns.push(cur);
|
|
27299
|
+
for (const fn of fns) {
|
|
27300
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27301
|
+
const before = fn.tainted.size;
|
|
27302
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27303
|
+
const t = lines[i2].trim();
|
|
27304
|
+
const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
|
|
27305
|
+
if (!m) continue;
|
|
27306
|
+
const lhs = m[1];
|
|
27307
|
+
const rhs = m[2];
|
|
27308
|
+
if (fn.tainted.has(lhs)) continue;
|
|
27309
|
+
for (const v of fn.tainted) {
|
|
27310
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27311
|
+
if (re.test(rhs)) {
|
|
27312
|
+
fn.tainted.add(lhs);
|
|
27313
|
+
break;
|
|
27314
|
+
}
|
|
27315
|
+
}
|
|
27316
|
+
}
|
|
27317
|
+
if (fn.tainted.size === before) break;
|
|
27318
|
+
}
|
|
27319
|
+
}
|
|
27320
|
+
const evalExprRe = /\bevalexpr\s*::\s*eval(?:_with_context|_boolean|_int|_float|_string|_tuple|_empty)?\s*\(\s*([^)]+)\s*\)/;
|
|
27321
|
+
const libloadingRe = /\blibloading\s*::\s*Library\s*::\s*new\s*\(\s*([^)]+)\s*\)/;
|
|
27322
|
+
const luaLoadRe = /\.\s*load\s*\(\s*([^)]+)\s*\)\s*\.\s*(?:exec|eval|call)\b/;
|
|
27323
|
+
for (const fn of fns) {
|
|
27324
|
+
if (fn.tainted.size === 0) continue;
|
|
27325
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27326
|
+
const line = lines[i2];
|
|
27327
|
+
const trimmed = line.trim();
|
|
27328
|
+
let arg = null;
|
|
27329
|
+
let formLabel = "";
|
|
27330
|
+
let m = trimmed.match(evalExprRe);
|
|
27331
|
+
if (m) {
|
|
27332
|
+
arg = m[1].trim();
|
|
27333
|
+
formLabel = "evalexpr::eval";
|
|
27334
|
+
}
|
|
27335
|
+
if (!arg) {
|
|
27336
|
+
m = trimmed.match(libloadingRe);
|
|
27337
|
+
if (m) {
|
|
27338
|
+
arg = m[1].trim();
|
|
27339
|
+
formLabel = "libloading::Library::new";
|
|
27340
|
+
}
|
|
27341
|
+
}
|
|
27342
|
+
if (!arg) {
|
|
27343
|
+
m = trimmed.match(luaLoadRe);
|
|
27344
|
+
if (m) {
|
|
27345
|
+
arg = m[1].trim();
|
|
27346
|
+
formLabel = "mlua/rlua Lua::load().{exec|eval|call}";
|
|
27347
|
+
}
|
|
27348
|
+
}
|
|
27349
|
+
if (arg === null) continue;
|
|
27350
|
+
if (arg.length === 0) continue;
|
|
27351
|
+
let unwrapped = arg.replace(/^&\s*/, "").trim();
|
|
27352
|
+
if (/^"[^"]*"$/.test(unwrapped)) continue;
|
|
27353
|
+
let tainted = false;
|
|
27354
|
+
for (const v of fn.tainted) {
|
|
27355
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
27356
|
+
if (re.test(unwrapped)) {
|
|
27357
|
+
tainted = true;
|
|
27358
|
+
break;
|
|
27359
|
+
}
|
|
27360
|
+
}
|
|
27361
|
+
if (!tainted) continue;
|
|
27362
|
+
findings.push({
|
|
27363
|
+
id: `code_injection-${file}-${i2 + 1}-rust-eval-crate`,
|
|
27364
|
+
pass: "language-sources",
|
|
27365
|
+
category: "security",
|
|
27366
|
+
rule_id: "code_injection",
|
|
27367
|
+
cwe: "CWE-94",
|
|
27368
|
+
severity: "critical",
|
|
27369
|
+
level: "error",
|
|
27370
|
+
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.`,
|
|
27371
|
+
file,
|
|
27372
|
+
line: i2 + 1,
|
|
27373
|
+
snippet: trimmed
|
|
27374
|
+
});
|
|
27375
|
+
}
|
|
27376
|
+
}
|
|
27377
|
+
return findings;
|
|
27378
|
+
}
|
|
27379
|
+
function findGoMongoNosqlInjectionFindings(code, file) {
|
|
27380
|
+
const findings = [];
|
|
27381
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27382
|
+
if (!/(\bbson\s*\.\s*[MDAE]\b|mongo-driver|\*\s*mongo\.Collection)/.test(code)) {
|
|
27383
|
+
return findings;
|
|
27384
|
+
}
|
|
27385
|
+
const lines = code.split("\n");
|
|
27386
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\s*\.\s*Query\s*\(\s*\)\s*\.\s*Get|Header\s*\.\s*Get|Cookie)\s*\(/;
|
|
27387
|
+
const httpReqParamRe = /\*\s*http\.Request\b/;
|
|
27388
|
+
const opsAlt = "(?:FindOne|Find|InsertOne|InsertMany|UpdateOne|UpdateMany|DeleteOne|DeleteMany|FindOneAndUpdate|FindOneAndDelete|FindOneAndReplace|Aggregate)";
|
|
27389
|
+
const callHeadRe = new RegExp(`\\.\\s*(${opsAlt})\\s*\\(`);
|
|
27390
|
+
function extractBalanced(line, openIdx) {
|
|
27391
|
+
let depth = 0;
|
|
27392
|
+
for (let k = openIdx; k < line.length; k++) {
|
|
27393
|
+
const ch = line[k];
|
|
27394
|
+
if (ch === "(") depth++;
|
|
27395
|
+
else if (ch === ")") {
|
|
27396
|
+
depth--;
|
|
27397
|
+
if (depth === 0) return line.substring(openIdx + 1, k);
|
|
27398
|
+
}
|
|
27399
|
+
}
|
|
27400
|
+
return null;
|
|
27401
|
+
}
|
|
27402
|
+
const funcs = [];
|
|
27403
|
+
let cur = null;
|
|
27404
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27405
|
+
const t = lines[i2].trim();
|
|
27406
|
+
if (/^func\b/.test(t)) {
|
|
27407
|
+
if (cur) {
|
|
27408
|
+
cur.end = i2 - 1;
|
|
27409
|
+
funcs.push(cur);
|
|
27410
|
+
}
|
|
27411
|
+
cur = { start: i2, end: lines.length - 1 };
|
|
27412
|
+
}
|
|
27413
|
+
}
|
|
27414
|
+
if (cur) funcs.push(cur);
|
|
27415
|
+
for (const fn of funcs) {
|
|
27416
|
+
const header = lines[fn.start];
|
|
27417
|
+
if (!httpReqParamRe.test(header)) continue;
|
|
27418
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27419
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27420
|
+
const before = taintedVars.size;
|
|
27421
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27422
|
+
const trimmed = lines[i2].trim();
|
|
27423
|
+
const assignMatch = trimmed.match(
|
|
27424
|
+
/^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/
|
|
27425
|
+
);
|
|
27426
|
+
if (!assignMatch) continue;
|
|
27427
|
+
const lhs = assignMatch[1];
|
|
27428
|
+
const rhs = assignMatch[2];
|
|
27429
|
+
if (taintedVars.has(lhs)) continue;
|
|
27430
|
+
if (reqExtractRe.test(rhs)) {
|
|
27431
|
+
taintedVars.add(lhs);
|
|
27432
|
+
continue;
|
|
27433
|
+
}
|
|
27434
|
+
for (const v of taintedVars) {
|
|
27435
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
27436
|
+
taintedVars.add(lhs);
|
|
27437
|
+
break;
|
|
27438
|
+
}
|
|
27439
|
+
}
|
|
27440
|
+
}
|
|
27441
|
+
if (taintedVars.size === before) break;
|
|
27442
|
+
}
|
|
27443
|
+
if (taintedVars.size === 0) continue;
|
|
27444
|
+
for (let i2 = fn.start; i2 <= fn.end; i2++) {
|
|
27445
|
+
const line = lines[i2];
|
|
27446
|
+
const m = callHeadRe.exec(line);
|
|
27447
|
+
if (!m) continue;
|
|
27448
|
+
const op = m[1];
|
|
27449
|
+
const openIdx = m.index + m[0].length - 1;
|
|
27450
|
+
const args2 = extractBalanced(line, openIdx);
|
|
27451
|
+
if (args2 === null || args2.length === 0) continue;
|
|
27452
|
+
let tainted = reqExtractRe.test(args2);
|
|
27453
|
+
if (!tainted) {
|
|
27454
|
+
for (const v of taintedVars) {
|
|
27455
|
+
if (new RegExp(`\\b${v}\\b`).test(args2)) {
|
|
27456
|
+
tainted = true;
|
|
27457
|
+
break;
|
|
27458
|
+
}
|
|
27459
|
+
}
|
|
27460
|
+
}
|
|
27461
|
+
if (!tainted) continue;
|
|
27462
|
+
findings.push({
|
|
27463
|
+
id: `nosql_injection-${file}-${i2 + 1}-go-mongo-${op.toLowerCase()}`,
|
|
27464
|
+
pass: "language-sources",
|
|
27465
|
+
category: "security",
|
|
27466
|
+
rule_id: "nosql_injection",
|
|
27467
|
+
cwe: "CWE-943",
|
|
27468
|
+
severity: "critical",
|
|
27469
|
+
level: "error",
|
|
27470
|
+
message: `NoSQL injection: Go MongoDB driver \`${op}(...)\` called with a filter derived from *http.Request input. Untrusted values inside a \`bson.M\` / \`bson.D\` filter can be operator objects (e.g. \`{"$ne": null}\`) and bypass authentication/intent. Validate or coerce the value to a primitive before building the filter.`,
|
|
27471
|
+
file,
|
|
27472
|
+
line: i2 + 1,
|
|
27473
|
+
snippet: line.trim()
|
|
27474
|
+
});
|
|
27475
|
+
}
|
|
27476
|
+
}
|
|
27477
|
+
return findings;
|
|
27478
|
+
}
|
|
27479
|
+
function findJavaMongoNosqlInjectionFindings(code, file) {
|
|
27480
|
+
const findings = [];
|
|
27481
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27482
|
+
if (!/(MongoCollection\b|com\.mongodb\b|\bFilters\b|\bnew\s+Document\s*\()/.test(code)) {
|
|
27483
|
+
return findings;
|
|
27484
|
+
}
|
|
27485
|
+
const lines = code.split("\n");
|
|
27486
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:getParameter|getParameterValues|getHeader|getHeaders|getCookies|getReader|getQueryString|getRequestURI|getInputStream|getPart|getParts)\s*\(/;
|
|
27487
|
+
const opsAlt = "(?:find|findOne|findOneAndUpdate|findOneAndDelete|findOneAndReplace|insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|replaceOne|aggregate|countDocuments|distinct)";
|
|
27488
|
+
const callRe = new RegExp(`\\.\\s*(${opsAlt})\\s*\\(([\\s\\S]*?)\\)`);
|
|
27489
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27490
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27491
|
+
const before = taintedVars.size;
|
|
27492
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27493
|
+
const t = lines[i2].trim();
|
|
27494
|
+
const a = t.match(/^(?:final\s+)?(?:[\w<>?,\[\]]+\s+)?(\w+)\s*=\s*(.+?);?$/);
|
|
27495
|
+
if (!a) continue;
|
|
27496
|
+
const lhs = a[1];
|
|
27497
|
+
const rhs = a[2];
|
|
27498
|
+
if (taintedVars.has(lhs)) continue;
|
|
27499
|
+
if (reqExtractRe.test(rhs)) {
|
|
27500
|
+
taintedVars.add(lhs);
|
|
27501
|
+
continue;
|
|
27502
|
+
}
|
|
27503
|
+
for (const v of taintedVars) {
|
|
27504
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
27505
|
+
taintedVars.add(lhs);
|
|
27506
|
+
break;
|
|
27507
|
+
}
|
|
27508
|
+
}
|
|
27509
|
+
}
|
|
27510
|
+
if (taintedVars.size === before) break;
|
|
27511
|
+
}
|
|
27512
|
+
if (taintedVars.size === 0) return findings;
|
|
27513
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27514
|
+
const line = lines[i2];
|
|
27515
|
+
const m = line.match(callRe);
|
|
27516
|
+
if (!m) continue;
|
|
27517
|
+
const op = m[1];
|
|
27518
|
+
const args2 = m[2];
|
|
27519
|
+
if (args2.trim().length === 0) continue;
|
|
27520
|
+
let tainted = reqExtractRe.test(args2);
|
|
27521
|
+
if (!tainted) {
|
|
27522
|
+
for (const v of taintedVars) {
|
|
27523
|
+
if (new RegExp(`\\b${v}\\b`).test(args2)) {
|
|
27524
|
+
tainted = true;
|
|
27525
|
+
break;
|
|
27526
|
+
}
|
|
27527
|
+
}
|
|
27528
|
+
}
|
|
27529
|
+
if (!tainted) continue;
|
|
27530
|
+
findings.push({
|
|
27531
|
+
id: `nosql_injection-${file}-${i2 + 1}-java-mongo-${op.toLowerCase()}`,
|
|
27532
|
+
pass: "language-sources",
|
|
27533
|
+
category: "security",
|
|
27534
|
+
rule_id: "nosql_injection",
|
|
27535
|
+
cwe: "CWE-943",
|
|
27536
|
+
severity: "critical",
|
|
27537
|
+
level: "error",
|
|
27538
|
+
message: `NoSQL injection: Java Mongo driver \`${op}(...)\` called with a filter derived from servlet request input. Untrusted values can reach BSON operator positions and bypass intent. Validate the input type before constructing the filter (e.g. require String).`,
|
|
27539
|
+
file,
|
|
27540
|
+
line: i2 + 1,
|
|
27541
|
+
snippet: line.trim()
|
|
27542
|
+
});
|
|
27543
|
+
}
|
|
27544
|
+
return findings;
|
|
27545
|
+
}
|
|
27546
|
+
function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
|
|
27547
|
+
const findings = [];
|
|
27548
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
27549
|
+
if (!/['"]\$where['"]/.test(code)) return findings;
|
|
27550
|
+
const lines = code.split("\n");
|
|
27551
|
+
const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
|
|
27552
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
27553
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
27554
|
+
const before = taintedVars.size;
|
|
27555
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27556
|
+
const t = lines[i2].trim();
|
|
27557
|
+
if (t.startsWith("#")) continue;
|
|
27558
|
+
const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
27559
|
+
if (!a) continue;
|
|
27560
|
+
const lhs = a[1];
|
|
27561
|
+
const rhs = a[2];
|
|
27562
|
+
if (taintedVars.has(lhs)) continue;
|
|
27563
|
+
if (reqExtractRe.test(rhs)) {
|
|
27564
|
+
taintedVars.add(lhs);
|
|
27565
|
+
continue;
|
|
27566
|
+
}
|
|
27567
|
+
for (const v of taintedVars) {
|
|
27568
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
27569
|
+
taintedVars.add(lhs);
|
|
27570
|
+
break;
|
|
27571
|
+
}
|
|
27572
|
+
}
|
|
27573
|
+
}
|
|
27574
|
+
if (taintedVars.size === before) break;
|
|
27575
|
+
}
|
|
27576
|
+
const whereRe = /['"]\$where['"]\s*:\s*([^,}\n]+)/;
|
|
27577
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27578
|
+
const line = lines[i2];
|
|
27579
|
+
const m = line.match(whereRe);
|
|
27580
|
+
if (!m) continue;
|
|
27581
|
+
const valueExpr = m[1].trim();
|
|
27582
|
+
if (/^"[^"]*"$/.test(valueExpr) || /^'[^']*'$/.test(valueExpr)) continue;
|
|
27583
|
+
let tainted = reqExtractRe.test(valueExpr);
|
|
27584
|
+
if (!tainted) {
|
|
27585
|
+
for (const v of taintedVars) {
|
|
27586
|
+
if (new RegExp(`\\b${v}\\b`).test(valueExpr)) {
|
|
27587
|
+
tainted = true;
|
|
27588
|
+
break;
|
|
27589
|
+
}
|
|
27590
|
+
}
|
|
27591
|
+
}
|
|
27592
|
+
if (!tainted) continue;
|
|
27593
|
+
findings.push({
|
|
27594
|
+
id: `nosql_injection-${file}-${i2 + 1}-py-mongoengine-where`,
|
|
27595
|
+
pass: "language-sources",
|
|
27596
|
+
category: "security",
|
|
27597
|
+
rule_id: "nosql_injection",
|
|
27598
|
+
cwe: "CWE-943",
|
|
27599
|
+
severity: "critical",
|
|
27600
|
+
level: "error",
|
|
27601
|
+
message: 'NoSQL injection: mongoengine `__raw__={"$where": ...}` payload derived from HTTP request input. The `$where` operator evaluates JavaScript on the server; tainted string concatenation lets an attacker inject arbitrary JS. Replace `$where` with field-based operators or validate the input.',
|
|
27602
|
+
file,
|
|
27603
|
+
line: i2 + 1,
|
|
27604
|
+
snippet: line.trim()
|
|
27605
|
+
});
|
|
27606
|
+
}
|
|
27607
|
+
return findings;
|
|
27608
|
+
}
|
|
26923
27609
|
|
|
26924
27610
|
// src/analysis/passes/sink-filter-pass.ts
|
|
26925
27611
|
var JS_XSS_SANITIZERS = [
|