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.
@@ -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.132.0",
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",