circle-ir 4.14.0 → 4.14.2

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.
Files changed (43) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +9 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/insecure-cookie-pass.d.ts +11 -4
  5. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  6. package/dist/analysis/passes/insecure-cookie-pass.js +30 -15
  7. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  8. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/interprocedural-pass.js +11 -2
  10. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +52 -0
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/scan-secrets-pass.js +24 -3
  16. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/sink-filter-pass.js +95 -38
  20. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  21. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  22. package/dist/analysis/passes/taint-propagation-pass.js +11 -3
  23. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  24. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  25. package/dist/analysis/taint-matcher.js +14 -5
  26. package/dist/analysis/taint-matcher.js.map +1 -1
  27. package/dist/analysis/taint-propagation.d.ts +7 -4
  28. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  29. package/dist/analysis/taint-propagation.js +132 -3
  30. package/dist/analysis/taint-propagation.js.map +1 -1
  31. package/dist/analyzer.js +1 -1
  32. package/dist/analyzer.js.map +1 -1
  33. package/dist/browser/circle-ir.js +339 -64
  34. package/dist/core/circle-ir-core.cjs +159 -15
  35. package/dist/core/circle-ir-core.js +159 -15
  36. package/dist/core/extractors/calls.js +19 -0
  37. package/dist/core/extractors/calls.js.map +1 -1
  38. package/dist/core/extractors/dfg.js +38 -8
  39. package/dist/core/extractors/dfg.js.map +1 -1
  40. package/dist/languages/plugins/go.d.ts.map +1 -1
  41. package/dist/languages/plugins/go.js +19 -0
  42. package/dist/languages/plugins/go.js.map +1 -1
  43. package/package.json +1 -1
@@ -8034,6 +8034,17 @@ function extractGoCallInfo(node) {
8034
8034
  if (operand.type === "identifier") {
8035
8035
  const resolved = resolveGoLocalReceiverType(opText, node);
8036
8036
  receiver = resolved !== null ? resolved : opText;
8037
+ } else if (operand.type === "selector_expression" && methodName === "Get") {
8038
+ const fieldNode = operand.childForFieldName("field");
8039
+ const inner = operand.childForFieldName("operand");
8040
+ const fieldName = fieldNode ? getNodeText(fieldNode) : "";
8041
+ const innerName = inner && inner.type === "identifier" ? getNodeText(inner) : null;
8042
+ const innerType = innerName !== null ? resolveGoLocalReceiverType(innerName, node) : null;
8043
+ if (innerType === "Request" && (fieldName === "PostForm" || fieldName === "Form")) {
8044
+ receiver = fieldName === "PostForm" ? "RequestPostForm" : "RequestForm";
8045
+ } else {
8046
+ receiver = opText;
8047
+ }
8037
8048
  } else {
8038
8049
  receiver = opText;
8039
8050
  }
@@ -10196,6 +10207,36 @@ function extractFieldDefs(body2, startId) {
10196
10207
  function extractJSParameterDefs(params, startId) {
10197
10208
  const defs = [];
10198
10209
  let id = startId;
10210
+ const addPatternDefs = (patternNode, line) => {
10211
+ if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
10212
+ defs.push({
10213
+ id: id++,
10214
+ variable: getNodeText(patternNode),
10215
+ line,
10216
+ kind: "param"
10217
+ });
10218
+ return;
10219
+ }
10220
+ if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
10221
+ const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
10222
+ if (!nameNode) return;
10223
+ addPatternDefs(nameNode, line);
10224
+ return;
10225
+ }
10226
+ if (patternNode.type === "assignment_pattern") {
10227
+ const leftNode = patternNode.childForFieldName("left");
10228
+ if (leftNode) addPatternDefs(leftNode, line);
10229
+ return;
10230
+ }
10231
+ if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
10232
+ const destructDefs = extractDestructuringDefs(patternNode, id);
10233
+ for (const def of destructDefs) {
10234
+ def.kind = "param";
10235
+ defs.push(def);
10236
+ id++;
10237
+ }
10238
+ }
10239
+ };
10199
10240
  for (let i2 = 0; i2 < params.childCount; i2++) {
10200
10241
  const param = params.child(i2);
10201
10242
  if (!param) continue;
@@ -10243,14 +10284,7 @@ function extractJSParameterDefs(params, startId) {
10243
10284
  }
10244
10285
  } else if (param.type === "required_parameter" || param.type === "optional_parameter") {
10245
10286
  const patternNode = param.childForFieldName("pattern");
10246
- if (patternNode && patternNode.type === "identifier") {
10247
- defs.push({
10248
- id: id++,
10249
- variable: getNodeText(patternNode),
10250
- line: param.startPosition.row + 1,
10251
- kind: "param"
10252
- });
10253
- }
10287
+ if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
10254
10288
  }
10255
10289
  }
10256
10290
  return defs;
@@ -13006,8 +13040,15 @@ var DEFAULT_SINKS = [
13006
13040
  { method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
13007
13041
  { method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
13008
13042
  { method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
13009
- // Wildfly/Undertow servlet resource manager
13010
- { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
13043
+ // Wildfly/Undertow servlet resource manager.
13044
+ // ResourceManager / PathResourceManager / FileResourceManager /
13045
+ // ClassPathResourceManager are the interface and implementations WildFly
13046
+ // calls through (cognium-dev#622, CVE-2018-1047). Class-qualified only.
13047
+ { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
13048
+ { method: "getResource", class: "ResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
13049
+ { method: "getResource", class: "PathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
13050
+ { method: "getResource", class: "FileResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
13051
+ { method: "getResource", class: "ClassPathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
13011
13052
  // Yamcs file system bucket
13012
13053
  { method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
13013
13054
  // RocketMQ validators
@@ -15766,7 +15807,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
15766
15807
  if (skipMethods.includes(method.name)) continue;
15767
15808
  for (const param of method.parameters) {
15768
15809
  const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
15769
- const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15810
+ const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
15811
+ const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15770
15812
  if (isTaintable) {
15771
15813
  const paramLine = param.line ?? method.start_line;
15772
15814
  sources.push({
@@ -15996,7 +16038,8 @@ function isInterproceduralTaintableType(typeName, language) {
15996
16038
  }
15997
16039
  function isParameterizedQueryCall(call, pattern) {
15998
16040
  if (pattern.type !== "sql_injection") return false;
15999
- const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
16041
+ const positions = pattern.arg_positions ?? [];
16042
+ const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
16000
16043
  const queryArg = call.arguments.find((a) => a.position === queryPos);
16001
16044
  if (queryArg) {
16002
16045
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
@@ -19255,6 +19298,9 @@ function buildSanitizersByLine(sanitizers) {
19255
19298
  }
19256
19299
  return out2;
19257
19300
  }
19301
+ function concatOutsideStrings(expression) {
19302
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
19303
+ }
19258
19304
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
19259
19305
  let graph;
19260
19306
  let sources;
@@ -19286,6 +19332,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
19286
19332
  }
19287
19333
  const taintedVars = [];
19288
19334
  const flows = [];
19335
+ const javaConcat = graph.ir.meta.language === "java";
19289
19336
  const reachableSinks = /* @__PURE__ */ new Map();
19290
19337
  const defsByLine = graph.defsByLine;
19291
19338
  const usesByLine = graph.usesByLine;
@@ -19322,23 +19369,40 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
19322
19369
  continue;
19323
19370
  }
19324
19371
  }
19325
- const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
19372
+ const expr = typeof arg.expression === "string" ? arg.expression : "";
19373
+ const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
19374
+ const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
19326
19375
  {
19327
19376
  for (const use of candidateUses) {
19328
19377
  if (use.def_id !== null) {
19329
19378
  if (allTaintedDefIds.has(use.def_id)) {
19330
- if (!arg.variable) {
19379
+ if (!arg.variable || compoundConcat) {
19331
19380
  if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
19332
19381
  const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
19333
19382
  if (!re.test(arg.expression)) continue;
19334
19383
  }
19335
19384
  const taintInfo = taintByDefId.get(use.def_id);
19336
19385
  if (taintInfo) {
19386
+ let sanMap = sanitizersByLine;
19387
+ if (identifierOutsideSanitizerCalls(
19388
+ use.variable,
19389
+ callsAtSink,
19390
+ sanitizersByLine.get(sink.line) ?? [],
19391
+ sink.type
19392
+ )) {
19393
+ const stripped = new Map(sanitizersByLine);
19394
+ const kept = (sanitizersByLine.get(sink.line) ?? []).filter(
19395
+ (s) => !sanitizerCoversSink(s, sink.type)
19396
+ );
19397
+ if (kept.length === 0) stripped.delete(sink.line);
19398
+ else stripped.set(sink.line, kept);
19399
+ sanMap = stripped;
19400
+ }
19337
19401
  const isSanitized = checkSanitized(
19338
19402
  taintInfo.line,
19339
19403
  sink.line,
19340
19404
  sink.type,
19341
- sanitizersByLine,
19405
+ sanMap,
19342
19406
  {
19343
19407
  startDefId: use.def_id,
19344
19408
  chainsByToDef: graph.chainsByToDef,
@@ -19492,6 +19556,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
19492
19556
  "crlf",
19493
19557
  "mass_assignment"
19494
19558
  ]);
19559
+ function readBalancedArg(text, openParen) {
19560
+ let depth = 0;
19561
+ let quote = null;
19562
+ let escaped = false;
19563
+ for (let i2 = openParen; i2 < text.length; i2++) {
19564
+ const c = text[i2];
19565
+ if (quote) {
19566
+ if (escaped) {
19567
+ escaped = false;
19568
+ continue;
19569
+ }
19570
+ if (c === "\\") {
19571
+ escaped = true;
19572
+ continue;
19573
+ }
19574
+ if (c === quote) quote = null;
19575
+ continue;
19576
+ }
19577
+ if (c === '"' || c === "'") {
19578
+ quote = c;
19579
+ continue;
19580
+ }
19581
+ if (c === "(") depth++;
19582
+ else if (c === ")") {
19583
+ depth--;
19584
+ if (depth === 0) return text.slice(openParen + 1, i2);
19585
+ }
19586
+ }
19587
+ return null;
19588
+ }
19589
+ function sanitizerCallRefs(sanitizers, sinkType) {
19590
+ const refs = [];
19591
+ for (const san of sanitizers) {
19592
+ if (!sanitizerCoversSink(san, sinkType)) continue;
19593
+ const m = /(?:(\w+)\.)?(\w+)\(\)/.exec(san.method);
19594
+ if (!m?.[2]) continue;
19595
+ refs.push({ receiver: m[1], methodName: m[2] });
19596
+ }
19597
+ return refs;
19598
+ }
19599
+ function maskSanitizerCalls(expr, refs) {
19600
+ let out2 = expr;
19601
+ for (const ref of refs) {
19602
+ const needles = ref.receiver ? [`${ref.receiver}.${ref.methodName}`, ref.methodName] : [ref.methodName];
19603
+ for (const needle of needles) {
19604
+ const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
19605
+ for (let guard = 0; guard < 8; guard++) {
19606
+ const m = re.exec(out2);
19607
+ if (!m) break;
19608
+ const open = m.index + m[0].length - 1;
19609
+ const arg = readBalancedArg(out2, open);
19610
+ if (arg === null) break;
19611
+ out2 = out2.slice(0, m.index) + " " + out2.slice(open + arg.length + 2);
19612
+ }
19613
+ }
19614
+ }
19615
+ return out2;
19616
+ }
19617
+ function blankStringLiterals(expr) {
19618
+ return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
19619
+ }
19620
+ function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType) {
19621
+ if (!/^[A-Za-z_$][\w$]*$/.test(variable)) return false;
19622
+ const refs = sanitizerCallRefs(sanitizers, sinkType);
19623
+ if (refs.length === 0) return false;
19624
+ const re = new RegExp(
19625
+ `(?:^|[^A-Za-z0-9_$])${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`
19626
+ );
19627
+ for (const call of calls) {
19628
+ if (refs.some((r) => r.methodName === call.method_name)) continue;
19629
+ for (const arg of call.arguments) {
19630
+ const expr = arg.expression ?? "";
19631
+ if (!expr) continue;
19632
+ const masked = maskSanitizerCalls(expr, refs);
19633
+ if (masked === expr) continue;
19634
+ if (re.test(blankStringLiterals(masked))) return true;
19635
+ }
19636
+ }
19637
+ return false;
19638
+ }
19495
19639
  function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
19496
19640
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
19497
19641
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -26397,6 +26541,25 @@ var GoPlugin = class extends BaseLanguagePlugin {
26397
26541
  confidence: 0.9,
26398
26542
  returnTainted: true
26399
26543
  },
26544
+ // #492 — `r.PostForm.Get` / `r.Form.Get`. The extractor rewrites only
26545
+ // those selectors on a declared `*http.Request` to these synthetic
26546
+ // classes, so `url.Values.Get` stays unmatched.
26547
+ {
26548
+ method: "Get",
26549
+ class: "RequestPostForm",
26550
+ type: "http_body",
26551
+ severity: "high",
26552
+ confidence: 0.9,
26553
+ returnTainted: true
26554
+ },
26555
+ {
26556
+ method: "Get",
26557
+ class: "RequestForm",
26558
+ type: "http_param",
26559
+ severity: "high",
26560
+ confidence: 0.9,
26561
+ returnTainted: true
26562
+ },
26400
26563
  // Gin framework
26401
26564
  // NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
26402
26565
  // registered identically in DEFAULT_SOURCES; the duplicates here emitted
@@ -28071,6 +28234,7 @@ var LanguageSourcesPass = class {
28071
28234
  additionalSources.push(...findGoMultipartUploadSources(code, language));
28072
28235
  additionalSources.push(...findGoGrpcRequestSources(code, language));
28073
28236
  additionalSources.push(...findGoRequestBodySources(code, language));
28237
+ additionalSources.push(...findGoFormIndexSources(code, language));
28074
28238
  const jsDOMSinks = findJavaScriptDOMSinks(code, language);
28075
28239
  for (const s of jsDOMSinks) {
28076
28240
  const alreadyExists = additionalSinks.some((x) => x.line === s.line && x.cwe === s.cwe);
@@ -28908,6 +29072,42 @@ function findGoGrpcRequestSources(sourceCode, language) {
28908
29072
  }
28909
29073
  return sources;
28910
29074
  }
29075
+ function findGoFormIndexSources(sourceCode, language) {
29076
+ if (language !== "go") return [];
29077
+ if (!/\.(?:PostForm|Form|MultipartForm)\b/.test(sourceCode)) return [];
29078
+ const lines = sourceCode.split("\n");
29079
+ const reqNames = /* @__PURE__ */ new Set();
29080
+ const paramRe = /([A-Za-z_]\w*)\s+\*http\.Request\b/g;
29081
+ for (const line of lines) {
29082
+ let m;
29083
+ paramRe.lastIndex = 0;
29084
+ while ((m = paramRe.exec(line)) !== null) reqNames.add(m[1]);
29085
+ }
29086
+ if (reqNames.size === 0) return [];
29087
+ const alt = [...reqNames].map((n) => n.replace(/\$/g, "\\$")).join("|");
29088
+ const indexRe = new RegExp(
29089
+ `^\\s*(?:var\\s+)?([A-Za-z_]\\w*)\\s*(?:,\\s*\\w+\\s*)?:?=\\s*(?:${alt})\\s*\\.\\s*(?:(PostForm|Form)|MultipartForm\\s*\\.\\s*Value)\\s*\\[`
29090
+ );
29091
+ const sources = [];
29092
+ for (let i2 = 0; i2 < lines.length; i2++) {
29093
+ const line = lines[i2];
29094
+ if (/^\s*\/\//.test(line)) continue;
29095
+ const m = indexRe.exec(line);
29096
+ if (!m) continue;
29097
+ const variable = m[1];
29098
+ if (variable === "_" || variable === "") continue;
29099
+ const kind = m[2];
29100
+ sources.push({
29101
+ type: kind === "Form" ? "http_param" : "http_body",
29102
+ location: "HTTP request form index",
29103
+ severity: "high",
29104
+ line: i2 + 1,
29105
+ confidence: 0.9,
29106
+ variable
29107
+ });
29108
+ }
29109
+ return sources;
29110
+ }
28911
29111
  function findGoRequestBodySources(sourceCode, language) {
28912
29112
  if (language !== "go") return [];
28913
29113
  if (!sourceCode.includes(".Body")) return [];
@@ -36330,7 +36530,7 @@ var SinkFilterPass = class {
36330
36530
  constProp.synchronizedLines,
36331
36531
  language
36332
36532
  );
36333
- filtered = filterSanitizedSinks(filtered, sanitizers, calls);
36533
+ filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
36334
36534
  if (language === "python") {
36335
36535
  const { pyTaintedVars, pySanitizedVars } = langSources;
36336
36536
  const sourceLines = ctx.code.split("\n");
@@ -36391,7 +36591,16 @@ var SinkFilterPass = class {
36391
36591
  filtered = filtered.filter((sink) => {
36392
36592
  if (sink.type !== "xss") return true;
36393
36593
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
36394
- if (JS_XSS_SANITIZERS.some((p) => p.test(sinkLineText))) return false;
36594
+ if (JS_XSS_SANITIZERS.some((p) => p.test(sinkLineText))) {
36595
+ const callsAtSink = callsByLine.get(sink.line) ?? [];
36596
+ const covering = sanitizers.filter(
36597
+ (s) => s.line === sink.line && sanitizerCoversSink(s, sink.type)
36598
+ );
36599
+ const rawOperand = sources.some(
36600
+ (s) => !!s.variable && identifierOutsideSanitizerCalls(s.variable, callsAtSink, covering, sink.type)
36601
+ );
36602
+ if (!rawOperand) return false;
36603
+ }
36395
36604
  const assignmentMatch = sinkLineText.match(/\.(?:innerHTML|outerHTML)\s*=\s*(.+)/);
36396
36605
  if (assignmentMatch) {
36397
36606
  const rhs = assignmentMatch[1].trim().replace(/;$/, "").trim();
@@ -37344,6 +37553,20 @@ function evaluateSimpleExpression(expr, symbols) {
37344
37553
  if (result !== null && !isNaN(result)) return String(Math.floor(result));
37345
37554
  return expr;
37346
37555
  }
37556
+ var ARGUMENT_IDENT_SKIP = /* @__PURE__ */ new Set(["new", "true", "false", "null", "this", "super"]);
37557
+ function isStringConcat(expression) {
37558
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
37559
+ }
37560
+ function argumentIdentifiers(expression) {
37561
+ const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
37562
+ const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
37563
+ const out2 = [];
37564
+ for (const id of found) {
37565
+ if (ARGUMENT_IDENT_SKIP.has(id)) continue;
37566
+ if (!out2.includes(id)) out2.push(id);
37567
+ }
37568
+ return out2;
37569
+ }
37347
37570
  function isStringLiteralExpression(expr) {
37348
37571
  const trimmed = expr.trim();
37349
37572
  if (trimmed.length < 2) return false;
@@ -37415,20 +37638,27 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
37415
37638
  dangerousArgCount++;
37416
37639
  if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
37417
37640
  if (arg.variable && !arg.expression?.includes("[")) {
37418
- const varName = arg.variable;
37419
- const scopedName = methodName ? `${methodName}:${varName}` : varName;
37420
- if (fieldNames.has(varName) && !isInSynchronizedBlock) {
37421
- allArgsAreClean = false;
37422
- continue;
37423
- }
37424
- if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
37425
- if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
37426
- allArgsAreClean = false;
37427
- continue;
37641
+ const expr = arg.expression ?? "";
37642
+ const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
37643
+ const names = concatNames.length > 0 ? concatNames : [arg.variable];
37644
+ let argClean = true;
37645
+ for (const varName of names) {
37646
+ const scopedName = methodName ? `${methodName}:${varName}` : varName;
37647
+ if (fieldNames.has(varName) && !isInSynchronizedBlock) {
37648
+ argClean = false;
37649
+ break;
37650
+ }
37651
+ if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
37652
+ if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
37653
+ argClean = false;
37654
+ break;
37655
+ }
37656
+ const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
37657
+ if (symbolValue && symbolValue.type !== "unknown") continue;
37658
+ argClean = false;
37659
+ break;
37428
37660
  }
37429
- const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
37430
- if (symbolValue && symbolValue.type !== "unknown") continue;
37431
- allArgsAreClean = false;
37661
+ if (!argClean) allArgsAreClean = false;
37432
37662
  } else {
37433
37663
  if (arg.literal != null) continue;
37434
37664
  if (arg.expression && !arg.variable && isStringLiteralExpression(arg.expression)) continue;
@@ -37440,8 +37670,12 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
37440
37670
  return true;
37441
37671
  });
37442
37672
  }
37443
- function filterSanitizedSinks(sinks, sanitizers, calls) {
37673
+ function filterSanitizedSinks(sinks, sanitizers, calls, sources) {
37444
37674
  if (!sanitizers || sanitizers.length === 0) return sinks;
37675
+ const sourceVars = [];
37676
+ for (const source of sources ?? []) {
37677
+ if (source.variable && /^[A-Za-z_$][\w$]*$/.test(source.variable)) sourceVars.push(source.variable);
37678
+ }
37445
37679
  const sanitizersByLine = /* @__PURE__ */ new Map();
37446
37680
  for (const san of sanitizers) {
37447
37681
  const existing = sanitizersByLine.get(san.line) ?? [];
@@ -37457,22 +37691,25 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
37457
37691
  return sinks.filter((sink) => {
37458
37692
  const lineSanitizers = sanitizersByLine.get(sink.line);
37459
37693
  if (!lineSanitizers || lineSanitizers.length === 0) return true;
37460
- for (const san of lineSanitizers) {
37461
- if (sanitizerCoversSink(san, sink.type)) {
37462
- const lineCalls = callsByLine.get(sink.line) ?? [];
37463
- for (const call of lineCalls) {
37464
- for (const arg of call.arguments) {
37465
- const expr = arg.expression || "";
37466
- const sanMethodMatch = san.method.match(/(?:(\w+)\.)?(\w+)\(\)/);
37467
- if (sanMethodMatch) {
37468
- const sanMethodName = sanMethodMatch[2];
37469
- const sanClassName = sanMethodMatch[1];
37470
- if (sanClassName) {
37471
- if (expr.includes(`${sanClassName}.${sanMethodName}(`)) return false;
37472
- } else if (expr.includes(`${sanMethodName}(`)) {
37473
- return false;
37474
- }
37475
- }
37694
+ const lineCalls = callsByLine.get(sink.line) ?? [];
37695
+ const covering = lineSanitizers.filter((san) => sanitizerCoversSink(san, sink.type));
37696
+ if (covering.length === 0) return true;
37697
+ for (const variable of sourceVars) {
37698
+ if (identifierOutsideSanitizerCalls(variable, lineCalls, covering, sink.type)) return true;
37699
+ }
37700
+ for (const san of covering) {
37701
+ const sanMethodMatch = san.method.match(/(?:(\w+)\.)?(\w+)\(\)/);
37702
+ if (!sanMethodMatch) continue;
37703
+ const sanMethodName = sanMethodMatch[2];
37704
+ const sanClassName = sanMethodMatch[1];
37705
+ for (const call of lineCalls) {
37706
+ if (call.method_name === sanMethodName) continue;
37707
+ for (const arg of call.arguments) {
37708
+ const expr = arg.expression || "";
37709
+ if (sanClassName) {
37710
+ if (expr.includes(`${sanClassName}.${sanMethodName}(`)) return false;
37711
+ } else if (expr.includes(`${sanMethodName}(`)) {
37712
+ return false;
37476
37713
  }
37477
37714
  }
37478
37715
  }
@@ -38828,9 +39065,13 @@ var TaintPropagationPass = class {
38828
39065
  const sansAtSink = sanitizersByLine.get(f.sink_line);
38829
39066
  if (sansAtSink && sansAtSink.length > 0) {
38830
39067
  for (const san of sansAtSink) {
38831
- if (sanitizerCoversSink(san, f.sink_type)) {
38832
- return false;
39068
+ if (!sanitizerCoversSink(san, f.sink_type)) continue;
39069
+ const sinkVar2 = f.path.length > 0 ? f.path[f.path.length - 1].variable : void 0;
39070
+ const callsAtSink = graph.callsByLine.get(f.sink_line) ?? [];
39071
+ if (sinkVar2 && identifierOutsideSanitizerCalls(sinkVar2, callsAtSink, sansAtSink, f.sink_type)) {
39072
+ continue;
38833
39073
  }
39074
+ return false;
38834
39075
  }
38835
39076
  }
38836
39077
  const usesAtSink = graph.usesByLine.get(f.sink_line);
@@ -40417,10 +40658,14 @@ var InterproceduralPass = class {
40417
40658
  }
40418
40659
  const sansAtSink = sanitizersByLine.get(f.sink_line);
40419
40660
  if (!sansAtSink || sansAtSink.length === 0) return true;
40661
+ const callsAtSink = graph.callsByLine.get(f.sink_line) ?? [];
40662
+ const sinkVar = f.path.length > 0 ? f.path[f.path.length - 1]?.variable : void 0;
40420
40663
  for (const san of sansAtSink) {
40421
- if (sanitizerCoversSink(san, f.sink_type)) {
40422
- return false;
40664
+ if (!sanitizerCoversSink(san, f.sink_type)) continue;
40665
+ if (sinkVar && identifierOutsideSanitizerCalls(sinkVar, callsAtSink, sansAtSink, f.sink_type)) {
40666
+ continue;
40423
40667
  }
40668
+ return false;
40424
40669
  }
40425
40670
  return true;
40426
40671
  });
@@ -44369,6 +44614,14 @@ var PROVIDER_PATTERNS = [
44369
44614
  level: "error",
44370
44615
  fix: "Revoke the GitHub refresh token and store secrets outside source control."
44371
44616
  },
44617
+ // Fine-grained PATs: `github_pat_` + 22 chars + `_` + 59 chars. (#553)
44618
+ {
44619
+ name: "GitHub fine-grained personal access token",
44620
+ regex: /\bgithub_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}\b/,
44621
+ severity: "critical",
44622
+ level: "error",
44623
+ fix: "Revoke the fine-grained personal access token at https://github.com/settings/tokens and store secrets outside source control."
44624
+ },
44372
44625
  // GitLab personal / project / group access tokens use the `glpat-` prefix.
44373
44626
  // Length is not fixed (legacy bodies are about 20 characters; routable
44374
44627
  // tokens are longer), so the floor is 20 and there is no max. (#577)
@@ -44397,12 +44650,14 @@ var PROVIDER_PATTERNS = [
44397
44650
  level: "error",
44398
44651
  fix: "Rotate the Stripe restricted key in the Stripe Dashboard and load it from a secrets manager."
44399
44652
  },
44653
+ // Publishable keys are meant to be embedded in a client. Keep the finding
44654
+ // so the key is still visible, but at informational severity. (#552)
44400
44655
  {
44401
44656
  name: "Stripe live publishable key",
44402
44657
  regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
44403
- severity: "high",
44404
- level: "warning",
44405
- fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
44658
+ severity: "low",
44659
+ level: "note",
44660
+ fix: "Publishable keys are not secret. Prefer loading them from configuration rather than a backend source file."
44406
44661
  },
44407
44662
  // Test-mode secret and restricted keys still authorize the test account.
44408
44663
  // Same length floor as the live forms. Severity is high, not critical (#576).
@@ -44427,6 +44682,17 @@ var PROVIDER_PATTERNS = [
44427
44682
  level: "error",
44428
44683
  fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
44429
44684
  },
44685
+ // Project keys (`sk-proj-`) are a different prefix from the legacy 48-char
44686
+ // secret. OpenAI does not publish one length, so the body floor is 48 and
44687
+ // there is no max and no entropy gate. The hyphen after `proj` keeps this
44688
+ // from matching the legacy pattern. (#551)
44689
+ {
44690
+ name: "OpenAI project API key",
44691
+ regex: /\bsk-proj-[A-Za-z0-9_-]{48,}\b/,
44692
+ severity: "critical",
44693
+ level: "error",
44694
+ fix: "Revoke the OpenAI project key at https://platform.openai.com/api-keys and load from environment."
44695
+ },
44430
44696
  {
44431
44697
  name: "Anthropic API key",
44432
44698
  regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
@@ -44719,7 +44985,7 @@ var ScanSecretsPass = class {
44719
44985
  for (const pattern of PROVIDER_PATTERNS) {
44720
44986
  const m = pattern.regex.exec(lineText);
44721
44987
  if (!m) continue;
44722
- if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
44988
+ if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token" || pattern.name === "OpenAI project API key" || pattern.name === "GitHub fine-grained personal access token") && PLACEHOLDER_RE.test(m[0])) {
44723
44989
  continue;
44724
44990
  }
44725
44991
  if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
@@ -45151,8 +45417,6 @@ var PY_SET_COOKIE_RECEIVERS = /* @__PURE__ */ new Set([
45151
45417
  ]);
45152
45418
  var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
45153
45419
  var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
45154
- var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
45155
- var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
45156
45420
  var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
45157
45421
  var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
45158
45422
  var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
@@ -45181,10 +45445,8 @@ var InsecureCookiePass = class {
45181
45445
  this.emit(ctx, file, det, "python");
45182
45446
  }
45183
45447
  } else if (language === "java") {
45184
- const hasSetSecureTrue = JAVA_SET_SECURE_TRUE_RE.test(code);
45185
- const hasSetHttpOnlyTrue = JAVA_SET_HTTPONLY_TRUE_RE.test(code);
45186
45448
  for (const call of graph.ir.calls) {
45187
- const det = this.detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue);
45449
+ const det = this.detectJavaCookieCtor(call, code);
45188
45450
  if (!det) continue;
45189
45451
  insecureCookies.push(det);
45190
45452
  this.emit(ctx, file, det, "java");
@@ -45263,15 +45525,28 @@ var InsecureCookiePass = class {
45263
45525
  };
45264
45526
  }
45265
45527
  // ---------------- Java ----------------
45266
- detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue) {
45528
+ /**
45529
+ * True when this cookie construction is followed by `var.setX(true)`.
45530
+ * The variable is the name assigned on the constructor line. An
45531
+ * unassigned `new Cookie(...)` only looks at that statement, so another
45532
+ * cookie's setter cannot cover it.
45533
+ */
45534
+ javaCookieSetterTrue(code, call, setter) {
45535
+ const line = code.split("\n")[(call.location.line ?? 1) - 1] ?? "";
45536
+ const assign = /\b([A-Za-z_]\w*)\s*=\s*new\s+(?:[\w.]+\.)?Cookie\b/.exec(line);
45537
+ const varName = assign?.[1];
45538
+ const re = varName ? new RegExp(`\\b${varName}\\.${setter}\\s*\\(\\s*true\\s*\\)`) : new RegExp(`\\.${setter}\\s*\\(\\s*true\\s*\\)`);
45539
+ return re.test(varName ? code : line);
45540
+ }
45541
+ detectJavaCookieCtor(call, code) {
45267
45542
  const method = call.method_name ?? "";
45268
45543
  const isCookieCtor = method === "Cookie" || method.endsWith(".Cookie");
45269
45544
  if (!isCookieCtor) return null;
45270
45545
  const looksLikeCtor = call.is_constructor || !call.receiver && (call.receiver_type === "Cookie" || (call.receiver_type ?? "").endsWith(".Cookie")) || (call.resolution?.target ?? "").endsWith(".<init>");
45271
45546
  if (!looksLikeCtor) return null;
45272
45547
  if (call.arguments.length < 2) return null;
45273
- const missingSecure = !hasSetSecureTrue;
45274
- const missingHttpOnly = !hasSetHttpOnlyTrue;
45548
+ const missingSecure = !this.javaCookieSetterTrue(code, call, "setSecure");
45549
+ const missingHttpOnly = !this.javaCookieSetterTrue(code, call, "setHttpOnly");
45275
45550
  if (!missingSecure && !missingHttpOnly) return null;
45276
45551
  return {
45277
45552
  line: call.location.line,
@@ -49885,7 +50160,7 @@ async function analyzeForAPI(code, filePath, language, options = {}) {
49885
50160
  constPropResult.sanitizedVars,
49886
50161
  constPropResult.synchronizedLines
49887
50162
  );
49888
- filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
50163
+ filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls, taint.sources);
49889
50164
  let pythonTaintedVars = /* @__PURE__ */ new Map();
49890
50165
  if (language === "python") {
49891
50166
  pythonTaintedVars = buildPythonTaintedVars(code);