circle-ir 3.132.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/passes/language-sources-pass.d.ts +39 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +566 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +438 -0
- package/package.json +1 -1
|
@@ -23672,6 +23672,12 @@ 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
|
+
}
|
|
23675
23681
|
}
|
|
23676
23682
|
if (language === "python") {
|
|
23677
23683
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -23696,6 +23702,12 @@ var LanguageSourcesPass = class {
|
|
|
23696
23702
|
)) {
|
|
23697
23703
|
ctx.addFinding(finding);
|
|
23698
23704
|
}
|
|
23705
|
+
for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(
|
|
23706
|
+
code,
|
|
23707
|
+
graph.ir.meta.file
|
|
23708
|
+
)) {
|
|
23709
|
+
ctx.addFinding(finding);
|
|
23710
|
+
}
|
|
23699
23711
|
}
|
|
23700
23712
|
if (language === "rust") {
|
|
23701
23713
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -23723,6 +23735,12 @@ var LanguageSourcesPass = class {
|
|
|
23723
23735
|
)) {
|
|
23724
23736
|
ctx.addFinding(finding);
|
|
23725
23737
|
}
|
|
23738
|
+
for (const finding of findRustEvalCrateCodeInjectionFindings(
|
|
23739
|
+
code,
|
|
23740
|
+
graph.ir.meta.file
|
|
23741
|
+
)) {
|
|
23742
|
+
ctx.addFinding(finding);
|
|
23743
|
+
}
|
|
23726
23744
|
}
|
|
23727
23745
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
23728
23746
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -23747,6 +23765,12 @@ var LanguageSourcesPass = class {
|
|
|
23747
23765
|
)) {
|
|
23748
23766
|
ctx.addFinding(finding);
|
|
23749
23767
|
}
|
|
23768
|
+
for (const finding of findJsIndirectEvalCodeInjectionFindings(
|
|
23769
|
+
code,
|
|
23770
|
+
graph.ir.meta.file
|
|
23771
|
+
)) {
|
|
23772
|
+
ctx.addFinding(finding);
|
|
23773
|
+
}
|
|
23750
23774
|
}
|
|
23751
23775
|
if (language === "java") {
|
|
23752
23776
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -26920,6 +26944,420 @@ function findJsDomOpenRedirectFindings(code, file) {
|
|
|
26920
26944
|
}
|
|
26921
26945
|
return out2;
|
|
26922
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
|
+
}
|
|
26923
27361
|
|
|
26924
27362
|
// src/analysis/passes/sink-filter-pass.ts
|
|
26925
27363
|
var JS_XSS_SANITIZERS = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.133.0",
|
|
4
4
|
"description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|