circle-ir 3.111.0 → 3.113.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.
@@ -11948,8 +11948,27 @@ var DEFAULT_SINKS = [
11948
11948
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
11949
11949
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
11950
11950
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11951
+ // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
11952
+ // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
11953
+ // target `Connection.all`, so class-scoped patterns matching `Connection`
11954
+ // hit (see #186 Sprint 55 matcher extension consulting call.resolution.target).
11955
+ // `exec`/`run`/`all`/`get`/`each` follow the same shape.
11956
+ { method: "all", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11957
+ { method: "run", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11958
+ { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11959
+ { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11960
+ { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11951
11961
  // Browser DOM XSS sinks
11952
11962
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11963
+ // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
11964
+ // These methods explicitly bypass Angular's built-in sanitizer; passing
11965
+ // tainted strings re-introduces DOM-injection risk. Distinctive method
11966
+ // names — classless + language-scoped is safe.
11967
+ { method: "bypassSecurityTrustHtml", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11968
+ { method: "bypassSecurityTrustScript", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11969
+ { method: "bypassSecurityTrustStyle", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11970
+ { method: "bypassSecurityTrustUrl", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11971
+ { method: "bypassSecurityTrustResourceUrl", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11953
11972
  // Express.js XSS (response methods)
11954
11973
  { method: "send", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11955
11974
  { method: "write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -11962,6 +11981,19 @@ var DEFAULT_SINKS = [
11962
11981
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11963
11982
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11964
11983
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11984
+ // `new vm.Script(taint)` — Node core `vm` module compiles strings. The
11985
+ // JS plugin emits method_name = 'vm.Script' for the constructor call;
11986
+ // the matcher's dotted-simple-name fallback (taint-matcher.ts:1664) lets
11987
+ // pattern.method = 'Script' hit on method_name = 'vm.Script'. The
11988
+ // `class: 'constructor'` short-circuit accepts the no-receiver shape.
11989
+ // (#188 Sprint 55)
11990
+ { method: "Script", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11991
+ // `setImmediate(taintedString)` — like setTimeout/setInterval, Node will
11992
+ // evaluate the first argument when it is a string. The callback-shape
11993
+ // suppression in taint-matcher.ts:1342 already covers setTimeout/
11994
+ // setInterval; the Sprint 55 fix extends that gate to setImmediate too.
11995
+ // (#188 Sprint 55)
11996
+ { method: "setImmediate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11965
11997
  // protobufjs Root.parse(schemaText) compiles a textual schema into JS at runtime;
11966
11998
  // tainted schema → code execution (CVE-2026-41242). Issue #94.
11967
11999
  { method: "parse", class: "protobuf", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -12024,6 +12056,14 @@ var DEFAULT_SINKS = [
12024
12056
  // got library
12025
12057
  { method: "get", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
12026
12058
  { method: "post", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
12059
+ // got/request npm packages default-export a callable function:
12060
+ // const got = require('got'); got(req.query.url)
12061
+ // const request = require('request'); request(req.query.url, cb)
12062
+ // The classless method names are distinctive enough (`got`, `request`) that
12063
+ // the FP risk is acceptable; both are scoped to JS/TS so they don't leak
12064
+ // into other plugins. (#185 Sprint 55)
12065
+ { method: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12066
+ { method: "request", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12027
12067
  // superagent
12028
12068
  { method: "get", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
12029
12069
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -12100,6 +12140,11 @@ var DEFAULT_SINKS = [
12100
12140
  { method: "exec", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
12101
12141
  { method: "compile", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
12102
12142
  { method: "__import__", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
12143
+ // Python dynamic import — `importlib.import_module(taint)` parallels Java's
12144
+ // `Class.forName`. The bare `__import__` entry above also matches the
12145
+ // `importlib.__import__` form because the sink-matcher is class-agnostic
12146
+ // when a classless entry exists. Sprint 56 #183.
12147
+ { method: "import_module", class: "importlib", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
12103
12148
  // Python Deserialization — language-scoped so the lowercase `yaml` / `pickle`
12104
12149
  // module names don't collide with Java locals named `yaml` (SnakeYAML usage).
12105
12150
  { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
@@ -12357,6 +12402,15 @@ var DEFAULT_SINKS = [
12357
12402
  // Standard library logging
12358
12403
  { method: "println!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
12359
12404
  { method: "eprintln!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
12405
+ // log:: namespaced forms — the Rust macro extractor preserves the full
12406
+ // path prefix in `method_name` (`log::info!`), so the bare entries above
12407
+ // only match the imported form `use log::info; info!(...)`. Sprint 56 #182 Slice A.
12408
+ { method: "log::info!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12409
+ { method: "log::warn!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12410
+ { method: "log::error!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12411
+ { method: "log::debug!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12412
+ { method: "log::trace!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12413
+ { method: "log::log!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
12360
12414
  // Rust sqlx SQL Injection
12361
12415
  { method: "query", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
12362
12416
  { method: "query_as", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
@@ -13538,7 +13592,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13538
13592
  }
13539
13593
  }
13540
13594
  }
13541
- if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout")) {
13595
+ if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout" || call.method_name === "setImmediate")) {
13542
13596
  const firstArg = call.arguments.find((a) => a.position === 0);
13543
13597
  if (firstArg && isFunctionCallbackArgument(firstArg)) {
13544
13598
  continue;
@@ -13855,6 +13909,11 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
13855
13909
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
13856
13910
  return true;
13857
13911
  }
13912
+ const resolvedTarget = call.resolution?.target;
13913
+ const expectedClassMethodTail = `${pattern.class}.${pattern.method}`;
13914
+ if (resolvedTarget && (resolvedTarget === expectedClassMethodTail || resolvedTarget.endsWith("." + expectedClassMethodTail))) {
13915
+ return true;
13916
+ }
13858
13917
  if (pattern.allow_unresolved_receiver && !call.receiver_type && !call.receiver_type_fqn && call.receiver.includes(".")) {
13859
13918
  return true;
13860
13919
  }
@@ -31280,6 +31339,9 @@ var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
31280
31339
  var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
31281
31340
  var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
31282
31341
  var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
31342
+ var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
31343
+ var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
31344
+ var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
31283
31345
  var InsecureCookiePass = class {
31284
31346
  name = "insecure-cookie";
31285
31347
  category = "security";
@@ -31310,6 +31372,19 @@ var InsecureCookiePass = class {
31310
31372
  insecureCookies.push(det);
31311
31373
  this.emit(ctx, file, det, "java");
31312
31374
  }
31375
+ } else if (language === "go") {
31376
+ for (const call of graph.ir.calls) {
31377
+ const det = this.detectGo(call);
31378
+ if (!det) continue;
31379
+ insecureCookies.push(det);
31380
+ this.emit(ctx, file, det, "go");
31381
+ }
31382
+ } else if (language === "rust") {
31383
+ const dets = this.detectRustSetCookieFormat(code);
31384
+ for (const det of dets) {
31385
+ insecureCookies.push(det);
31386
+ this.emit(ctx, file, det, "rust");
31387
+ }
31313
31388
  }
31314
31389
  return { insecureCookies };
31315
31390
  }
@@ -31369,11 +31444,60 @@ var InsecureCookiePass = class {
31369
31444
  optionsPresent: false
31370
31445
  };
31371
31446
  }
31447
+ // ---------------- Go ----------------
31448
+ detectGo(call) {
31449
+ if (call.method_name !== "SetCookie") return null;
31450
+ if ((call.receiver ?? "") !== "http") return null;
31451
+ if (call.arguments.length < 2) return null;
31452
+ const cookieArg = call.arguments.find((a) => a.position === 1);
31453
+ const cookieExpr = (cookieArg?.expression ?? "").trim();
31454
+ if (!cookieExpr.includes("{") || !cookieExpr.includes("}")) return null;
31455
+ const missingSecure = !GO_SECURE_TRUE_RE.test(cookieExpr);
31456
+ const missingHttpOnly = !GO_HTTPONLY_TRUE_RE.test(cookieExpr);
31457
+ if (!missingSecure && !missingHttpOnly) return null;
31458
+ return {
31459
+ line: call.location.line,
31460
+ receiver: "http.SetCookie",
31461
+ missingSecure,
31462
+ missingHttpOnly,
31463
+ optionsPresent: true
31464
+ };
31465
+ }
31466
+ // ---------------- Rust ----------------
31467
+ detectRustSetCookieFormat(code) {
31468
+ const out2 = [];
31469
+ const re = new RegExp(RUST_SET_COOKIE_MACRO_RE.source, RUST_SET_COOKIE_MACRO_RE.flags);
31470
+ let m;
31471
+ while ((m = re.exec(code)) !== null) {
31472
+ const macro = m[1] ?? "";
31473
+ const body2 = m[2] ?? "";
31474
+ const missingSecure = !/\bSecure\b/.test(body2);
31475
+ const missingHttpOnly = !/\bHttpOnly\b/.test(body2);
31476
+ if (!missingSecure && !missingHttpOnly) continue;
31477
+ const line = code.slice(0, m.index).split("\n").length;
31478
+ out2.push({
31479
+ line,
31480
+ receiver: macro,
31481
+ missingSecure,
31482
+ missingHttpOnly,
31483
+ optionsPresent: true
31484
+ });
31485
+ }
31486
+ return out2;
31487
+ }
31372
31488
  emit(ctx, file, det, flavor) {
31373
31489
  const missing = [];
31374
- if (det.missingSecure) missing.push(flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : "`setSecure(true)`");
31375
- if (det.missingHttpOnly) missing.push(flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : "`setHttpOnly(true)`");
31376
- const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response.";
31490
+ if (det.missingSecure) {
31491
+ missing.push(
31492
+ flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : flavor === "java" ? "`setSecure(true)`" : flavor === "go" ? "`Secure: true`" : "`Secure` attribute"
31493
+ );
31494
+ }
31495
+ if (det.missingHttpOnly) {
31496
+ missing.push(
31497
+ flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : flavor === "java" ? "`setHttpOnly(true)`" : flavor === "go" ? "`HttpOnly: true`" : "`HttpOnly` attribute"
31498
+ );
31499
+ }
31500
+ const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : flavor === "java" ? "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response." : flavor === "go" ? "Set `Secure: true` and `HttpOnly: true` on the `http.Cookie` struct literal passed to `http.SetCookie`." : "Append `; Secure; HttpOnly` to the `Set-Cookie` header string.";
31377
31501
  ctx.addFinding({
31378
31502
  id: `${this.name}-${file}-${det.line}`,
31379
31503
  pass: this.name,
@@ -11330,8 +11330,27 @@ var DEFAULT_SINKS = [
11330
11330
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
11331
11331
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
11332
11332
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11333
+ // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
11334
+ // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
11335
+ // target `Connection.all`, so class-scoped patterns matching `Connection`
11336
+ // hit (see #186 Sprint 55 matcher extension consulting call.resolution.target).
11337
+ // `exec`/`run`/`all`/`get`/`each` follow the same shape.
11338
+ { method: "all", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11339
+ { method: "run", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11340
+ { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11341
+ { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11342
+ { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11333
11343
  // Browser DOM XSS sinks
11334
11344
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11345
+ // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
11346
+ // These methods explicitly bypass Angular's built-in sanitizer; passing
11347
+ // tainted strings re-introduces DOM-injection risk. Distinctive method
11348
+ // names — classless + language-scoped is safe.
11349
+ { method: "bypassSecurityTrustHtml", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11350
+ { method: "bypassSecurityTrustScript", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11351
+ { method: "bypassSecurityTrustStyle", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11352
+ { method: "bypassSecurityTrustUrl", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11353
+ { method: "bypassSecurityTrustResourceUrl", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11335
11354
  // Express.js XSS (response methods)
11336
11355
  { method: "send", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11337
11356
  { method: "write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -11344,6 +11363,19 @@ var DEFAULT_SINKS = [
11344
11363
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11345
11364
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11346
11365
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11366
+ // `new vm.Script(taint)` — Node core `vm` module compiles strings. The
11367
+ // JS plugin emits method_name = 'vm.Script' for the constructor call;
11368
+ // the matcher's dotted-simple-name fallback (taint-matcher.ts:1664) lets
11369
+ // pattern.method = 'Script' hit on method_name = 'vm.Script'. The
11370
+ // `class: 'constructor'` short-circuit accepts the no-receiver shape.
11371
+ // (#188 Sprint 55)
11372
+ { method: "Script", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11373
+ // `setImmediate(taintedString)` — like setTimeout/setInterval, Node will
11374
+ // evaluate the first argument when it is a string. The callback-shape
11375
+ // suppression in taint-matcher.ts:1342 already covers setTimeout/
11376
+ // setInterval; the Sprint 55 fix extends that gate to setImmediate too.
11377
+ // (#188 Sprint 55)
11378
+ { method: "setImmediate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11347
11379
  // protobufjs Root.parse(schemaText) compiles a textual schema into JS at runtime;
11348
11380
  // tainted schema → code execution (CVE-2026-41242). Issue #94.
11349
11381
  { method: "parse", class: "protobuf", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -11406,6 +11438,14 @@ var DEFAULT_SINKS = [
11406
11438
  // got library
11407
11439
  { method: "get", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11408
11440
  { method: "post", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11441
+ // got/request npm packages default-export a callable function:
11442
+ // const got = require('got'); got(req.query.url)
11443
+ // const request = require('request'); request(req.query.url, cb)
11444
+ // The classless method names are distinctive enough (`got`, `request`) that
11445
+ // the FP risk is acceptable; both are scoped to JS/TS so they don't leak
11446
+ // into other plugins. (#185 Sprint 55)
11447
+ { method: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11448
+ { method: "request", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11409
11449
  // superagent
11410
11450
  { method: "get", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11411
11451
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11482,6 +11522,11 @@ var DEFAULT_SINKS = [
11482
11522
  { method: "exec", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11483
11523
  { method: "compile", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11484
11524
  { method: "__import__", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11525
+ // Python dynamic import — `importlib.import_module(taint)` parallels Java's
11526
+ // `Class.forName`. The bare `__import__` entry above also matches the
11527
+ // `importlib.__import__` form because the sink-matcher is class-agnostic
11528
+ // when a classless entry exists. Sprint 56 #183.
11529
+ { method: "import_module", class: "importlib", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11485
11530
  // Python Deserialization — language-scoped so the lowercase `yaml` / `pickle`
11486
11531
  // module names don't collide with Java locals named `yaml` (SnakeYAML usage).
11487
11532
  { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
@@ -11739,6 +11784,15 @@ var DEFAULT_SINKS = [
11739
11784
  // Standard library logging
11740
11785
  { method: "println!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
11741
11786
  { method: "eprintln!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
11787
+ // log:: namespaced forms — the Rust macro extractor preserves the full
11788
+ // path prefix in `method_name` (`log::info!`), so the bare entries above
11789
+ // only match the imported form `use log::info; info!(...)`. Sprint 56 #182 Slice A.
11790
+ { method: "log::info!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11791
+ { method: "log::warn!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11792
+ { method: "log::error!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11793
+ { method: "log::debug!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11794
+ { method: "log::trace!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11795
+ { method: "log::log!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11742
11796
  // Rust sqlx SQL Injection
11743
11797
  { method: "query", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
11744
11798
  { method: "query_as", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
@@ -12833,7 +12887,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12833
12887
  }
12834
12888
  }
12835
12889
  }
12836
- if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout")) {
12890
+ if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout" || call.method_name === "setImmediate")) {
12837
12891
  const firstArg = call.arguments.find((a) => a.position === 0);
12838
12892
  if (firstArg && isFunctionCallbackArgument(firstArg)) {
12839
12893
  continue;
@@ -13150,6 +13204,11 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
13150
13204
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
13151
13205
  return true;
13152
13206
  }
13207
+ const resolvedTarget = call.resolution?.target;
13208
+ const expectedClassMethodTail = `${pattern.class}.${pattern.method}`;
13209
+ if (resolvedTarget && (resolvedTarget === expectedClassMethodTail || resolvedTarget.endsWith("." + expectedClassMethodTail))) {
13210
+ return true;
13211
+ }
13153
13212
  if (pattern.allow_unresolved_receiver && !call.receiver_type && !call.receiver_type_fqn && call.receiver.includes(".")) {
13154
13213
  return true;
13155
13214
  }
@@ -11264,8 +11264,27 @@ var DEFAULT_SINKS = [
11264
11264
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
11265
11265
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
11266
11266
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11267
+ // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
11268
+ // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
11269
+ // target `Connection.all`, so class-scoped patterns matching `Connection`
11270
+ // hit (see #186 Sprint 55 matcher extension consulting call.resolution.target).
11271
+ // `exec`/`run`/`all`/`get`/`each` follow the same shape.
11272
+ { method: "all", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11273
+ { method: "run", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11274
+ { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11275
+ { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11276
+ { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
11267
11277
  // Browser DOM XSS sinks
11268
11278
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11279
+ // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
11280
+ // These methods explicitly bypass Angular's built-in sanitizer; passing
11281
+ // tainted strings re-introduces DOM-injection risk. Distinctive method
11282
+ // names — classless + language-scoped is safe.
11283
+ { method: "bypassSecurityTrustHtml", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11284
+ { method: "bypassSecurityTrustScript", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11285
+ { method: "bypassSecurityTrustStyle", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11286
+ { method: "bypassSecurityTrustUrl", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11287
+ { method: "bypassSecurityTrustResourceUrl", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11269
11288
  // Express.js XSS (response methods)
11270
11289
  { method: "send", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11271
11290
  { method: "write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -11278,6 +11297,19 @@ var DEFAULT_SINKS = [
11278
11297
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11279
11298
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11280
11299
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11300
+ // `new vm.Script(taint)` — Node core `vm` module compiles strings. The
11301
+ // JS plugin emits method_name = 'vm.Script' for the constructor call;
11302
+ // the matcher's dotted-simple-name fallback (taint-matcher.ts:1664) lets
11303
+ // pattern.method = 'Script' hit on method_name = 'vm.Script'. The
11304
+ // `class: 'constructor'` short-circuit accepts the no-receiver shape.
11305
+ // (#188 Sprint 55)
11306
+ { method: "Script", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11307
+ // `setImmediate(taintedString)` — like setTimeout/setInterval, Node will
11308
+ // evaluate the first argument when it is a string. The callback-shape
11309
+ // suppression in taint-matcher.ts:1342 already covers setTimeout/
11310
+ // setInterval; the Sprint 55 fix extends that gate to setImmediate too.
11311
+ // (#188 Sprint 55)
11312
+ { method: "setImmediate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11281
11313
  // protobufjs Root.parse(schemaText) compiles a textual schema into JS at runtime;
11282
11314
  // tainted schema → code execution (CVE-2026-41242). Issue #94.
11283
11315
  { method: "parse", class: "protobuf", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -11340,6 +11372,14 @@ var DEFAULT_SINKS = [
11340
11372
  // got library
11341
11373
  { method: "get", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11342
11374
  { method: "post", class: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11375
+ // got/request npm packages default-export a callable function:
11376
+ // const got = require('got'); got(req.query.url)
11377
+ // const request = require('request'); request(req.query.url, cb)
11378
+ // The classless method names are distinctive enough (`got`, `request`) that
11379
+ // the FP risk is acceptable; both are scoped to JS/TS so they don't leak
11380
+ // into other plugins. (#185 Sprint 55)
11381
+ { method: "got", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11382
+ { method: "request", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11343
11383
  // superagent
11344
11384
  { method: "get", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11345
11385
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11416,6 +11456,11 @@ var DEFAULT_SINKS = [
11416
11456
  { method: "exec", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11417
11457
  { method: "compile", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11418
11458
  { method: "__import__", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11459
+ // Python dynamic import — `importlib.import_module(taint)` parallels Java's
11460
+ // `Class.forName`. The bare `__import__` entry above also matches the
11461
+ // `importlib.__import__` form because the sink-matcher is class-agnostic
11462
+ // when a classless entry exists. Sprint 56 #183.
11463
+ { method: "import_module", class: "importlib", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0], languages: ["python"] },
11419
11464
  // Python Deserialization — language-scoped so the lowercase `yaml` / `pickle`
11420
11465
  // module names don't collide with Java locals named `yaml` (SnakeYAML usage).
11421
11466
  { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
@@ -11673,6 +11718,15 @@ var DEFAULT_SINKS = [
11673
11718
  // Standard library logging
11674
11719
  { method: "println!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
11675
11720
  { method: "eprintln!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2] },
11721
+ // log:: namespaced forms — the Rust macro extractor preserves the full
11722
+ // path prefix in `method_name` (`log::info!`), so the bare entries above
11723
+ // only match the imported form `use log::info; info!(...)`. Sprint 56 #182 Slice A.
11724
+ { method: "log::info!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11725
+ { method: "log::warn!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11726
+ { method: "log::error!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11727
+ { method: "log::debug!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11728
+ { method: "log::trace!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11729
+ { method: "log::log!", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2], languages: ["rust"] },
11676
11730
  // Rust sqlx SQL Injection
11677
11731
  { method: "query", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
11678
11732
  { method: "query_as", class: "sqlx", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0] },
@@ -12767,7 +12821,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12767
12821
  }
12768
12822
  }
12769
12823
  }
12770
- if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout")) {
12824
+ if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout" || call.method_name === "setImmediate")) {
12771
12825
  const firstArg = call.arguments.find((a) => a.position === 0);
12772
12826
  if (firstArg && isFunctionCallbackArgument(firstArg)) {
12773
12827
  continue;
@@ -13084,6 +13138,11 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
13084
13138
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
13085
13139
  return true;
13086
13140
  }
13141
+ const resolvedTarget = call.resolution?.target;
13142
+ const expectedClassMethodTail = `${pattern.class}.${pattern.method}`;
13143
+ if (resolvedTarget && (resolvedTarget === expectedClassMethodTail || resolvedTarget.endsWith("." + expectedClassMethodTail))) {
13144
+ return true;
13145
+ }
13087
13146
  if (pattern.allow_unresolved_receiver && !call.receiver_type && !call.receiver_type_fqn && call.receiver.includes(".")) {
13088
13147
  return true;
13089
13148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.111.0",
3
+ "version": "3.113.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",