circle-ir 4.14.1 → 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 (41) 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 +48 -24
  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 +5 -2
  26. package/dist/analysis/taint-matcher.js.map +1 -1
  27. package/dist/analysis/taint-propagation.d.ts +9 -0
  28. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  29. package/dist/analysis/taint-propagation.js +120 -1
  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 +264 -40
  34. package/dist/core/circle-ir-core.cjs +118 -4
  35. package/dist/core/circle-ir-core.js +118 -4
  36. package/dist/core/extractors/calls.js +19 -0
  37. package/dist/core/extractors/calls.js.map +1 -1
  38. package/dist/languages/plugins/go.d.ts.map +1 -1
  39. package/dist/languages/plugins/go.js +19 -0
  40. package/dist/languages/plugins/go.js.map +1 -1
  41. 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
  }
@@ -13029,8 +13040,15 @@ var DEFAULT_SINKS = [
13029
13040
  { method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
13030
13041
  { method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
13031
13042
  { method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
13032
- // Wildfly/Undertow servlet resource manager
13033
- { 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"] },
13034
13052
  // Yamcs file system bucket
13035
13053
  { method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
13036
13054
  // RocketMQ validators
@@ -16020,7 +16038,8 @@ function isInterproceduralTaintableType(typeName, language) {
16020
16038
  }
16021
16039
  function isParameterizedQueryCall(call, pattern) {
16022
16040
  if (pattern.type !== "sql_injection") return false;
16023
- 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;
16024
16043
  const queryArg = call.arguments.find((a) => a.position === queryPos);
16025
16044
  if (queryArg) {
16026
16045
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
@@ -19364,11 +19383,26 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
19364
19383
  }
19365
19384
  const taintInfo = taintByDefId.get(use.def_id);
19366
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
+ }
19367
19401
  const isSanitized = checkSanitized(
19368
19402
  taintInfo.line,
19369
19403
  sink.line,
19370
19404
  sink.type,
19371
- sanitizersByLine,
19405
+ sanMap,
19372
19406
  {
19373
19407
  startDefId: use.def_id,
19374
19408
  chainsByToDef: graph.chainsByToDef,
@@ -19522,6 +19556,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
19522
19556
  "crlf",
19523
19557
  "mass_assignment"
19524
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
+ }
19525
19639
  function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
19526
19640
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
19527
19641
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -26427,6 +26541,25 @@ var GoPlugin = class extends BaseLanguagePlugin {
26427
26541
  confidence: 0.9,
26428
26542
  returnTainted: true
26429
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
+ },
26430
26563
  // Gin framework
26431
26564
  // NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
26432
26565
  // registered identically in DEFAULT_SOURCES; the duplicates here emitted
@@ -28101,6 +28234,7 @@ var LanguageSourcesPass = class {
28101
28234
  additionalSources.push(...findGoMultipartUploadSources(code, language));
28102
28235
  additionalSources.push(...findGoGrpcRequestSources(code, language));
28103
28236
  additionalSources.push(...findGoRequestBodySources(code, language));
28237
+ additionalSources.push(...findGoFormIndexSources(code, language));
28104
28238
  const jsDOMSinks = findJavaScriptDOMSinks(code, language);
28105
28239
  for (const s of jsDOMSinks) {
28106
28240
  const alreadyExists = additionalSinks.some((x) => x.line === s.line && x.cwe === s.cwe);
@@ -28938,6 +29072,42 @@ function findGoGrpcRequestSources(sourceCode, language) {
28938
29072
  }
28939
29073
  return sources;
28940
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
+ }
28941
29111
  function findGoRequestBodySources(sourceCode, language) {
28942
29112
  if (language !== "go") return [];
28943
29113
  if (!sourceCode.includes(".Body")) return [];
@@ -36360,7 +36530,7 @@ var SinkFilterPass = class {
36360
36530
  constProp.synchronizedLines,
36361
36531
  language
36362
36532
  );
36363
- filtered = filterSanitizedSinks(filtered, sanitizers, calls);
36533
+ filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
36364
36534
  if (language === "python") {
36365
36535
  const { pyTaintedVars, pySanitizedVars } = langSources;
36366
36536
  const sourceLines = ctx.code.split("\n");
@@ -36421,7 +36591,16 @@ var SinkFilterPass = class {
36421
36591
  filtered = filtered.filter((sink) => {
36422
36592
  if (sink.type !== "xss") return true;
36423
36593
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
36424
- 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
+ }
36425
36604
  const assignmentMatch = sinkLineText.match(/\.(?:innerHTML|outerHTML)\s*=\s*(.+)/);
36426
36605
  if (assignmentMatch) {
36427
36606
  const rhs = assignmentMatch[1].trim().replace(/;$/, "").trim();
@@ -37491,8 +37670,12 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
37491
37670
  return true;
37492
37671
  });
37493
37672
  }
37494
- function filterSanitizedSinks(sinks, sanitizers, calls) {
37673
+ function filterSanitizedSinks(sinks, sanitizers, calls, sources) {
37495
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
+ }
37496
37679
  const sanitizersByLine = /* @__PURE__ */ new Map();
37497
37680
  for (const san of sanitizers) {
37498
37681
  const existing = sanitizersByLine.get(san.line) ?? [];
@@ -37508,22 +37691,25 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
37508
37691
  return sinks.filter((sink) => {
37509
37692
  const lineSanitizers = sanitizersByLine.get(sink.line);
37510
37693
  if (!lineSanitizers || lineSanitizers.length === 0) return true;
37511
- for (const san of lineSanitizers) {
37512
- if (sanitizerCoversSink(san, sink.type)) {
37513
- const lineCalls = callsByLine.get(sink.line) ?? [];
37514
- for (const call of lineCalls) {
37515
- for (const arg of call.arguments) {
37516
- const expr = arg.expression || "";
37517
- const sanMethodMatch = san.method.match(/(?:(\w+)\.)?(\w+)\(\)/);
37518
- if (sanMethodMatch) {
37519
- const sanMethodName = sanMethodMatch[2];
37520
- const sanClassName = sanMethodMatch[1];
37521
- if (sanClassName) {
37522
- if (expr.includes(`${sanClassName}.${sanMethodName}(`)) return false;
37523
- } else if (expr.includes(`${sanMethodName}(`)) {
37524
- return false;
37525
- }
37526
- }
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;
37527
37713
  }
37528
37714
  }
37529
37715
  }
@@ -38879,9 +39065,13 @@ var TaintPropagationPass = class {
38879
39065
  const sansAtSink = sanitizersByLine.get(f.sink_line);
38880
39066
  if (sansAtSink && sansAtSink.length > 0) {
38881
39067
  for (const san of sansAtSink) {
38882
- if (sanitizerCoversSink(san, f.sink_type)) {
38883
- 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;
38884
39073
  }
39074
+ return false;
38885
39075
  }
38886
39076
  }
38887
39077
  const usesAtSink = graph.usesByLine.get(f.sink_line);
@@ -40468,10 +40658,14 @@ var InterproceduralPass = class {
40468
40658
  }
40469
40659
  const sansAtSink = sanitizersByLine.get(f.sink_line);
40470
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;
40471
40663
  for (const san of sansAtSink) {
40472
- if (sanitizerCoversSink(san, f.sink_type)) {
40473
- return false;
40664
+ if (!sanitizerCoversSink(san, f.sink_type)) continue;
40665
+ if (sinkVar && identifierOutsideSanitizerCalls(sinkVar, callsAtSink, sansAtSink, f.sink_type)) {
40666
+ continue;
40474
40667
  }
40668
+ return false;
40475
40669
  }
40476
40670
  return true;
40477
40671
  });
@@ -44420,6 +44614,14 @@ var PROVIDER_PATTERNS = [
44420
44614
  level: "error",
44421
44615
  fix: "Revoke the GitHub refresh token and store secrets outside source control."
44422
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
+ },
44423
44625
  // GitLab personal / project / group access tokens use the `glpat-` prefix.
44424
44626
  // Length is not fixed (legacy bodies are about 20 characters; routable
44425
44627
  // tokens are longer), so the floor is 20 and there is no max. (#577)
@@ -44448,12 +44650,14 @@ var PROVIDER_PATTERNS = [
44448
44650
  level: "error",
44449
44651
  fix: "Rotate the Stripe restricted key in the Stripe Dashboard and load it from a secrets manager."
44450
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)
44451
44655
  {
44452
44656
  name: "Stripe live publishable key",
44453
44657
  regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
44454
- severity: "high",
44455
- level: "warning",
44456
- 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."
44457
44661
  },
44458
44662
  // Test-mode secret and restricted keys still authorize the test account.
44459
44663
  // Same length floor as the live forms. Severity is high, not critical (#576).
@@ -44478,6 +44682,17 @@ var PROVIDER_PATTERNS = [
44478
44682
  level: "error",
44479
44683
  fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
44480
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
+ },
44481
44696
  {
44482
44697
  name: "Anthropic API key",
44483
44698
  regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
@@ -44770,7 +44985,7 @@ var ScanSecretsPass = class {
44770
44985
  for (const pattern of PROVIDER_PATTERNS) {
44771
44986
  const m = pattern.regex.exec(lineText);
44772
44987
  if (!m) continue;
44773
- 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])) {
44774
44989
  continue;
44775
44990
  }
44776
44991
  if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
@@ -45202,8 +45417,6 @@ var PY_SET_COOKIE_RECEIVERS = /* @__PURE__ */ new Set([
45202
45417
  ]);
45203
45418
  var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
45204
45419
  var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
45205
- var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
45206
- var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
45207
45420
  var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
45208
45421
  var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
45209
45422
  var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
@@ -45232,10 +45445,8 @@ var InsecureCookiePass = class {
45232
45445
  this.emit(ctx, file, det, "python");
45233
45446
  }
45234
45447
  } else if (language === "java") {
45235
- const hasSetSecureTrue = JAVA_SET_SECURE_TRUE_RE.test(code);
45236
- const hasSetHttpOnlyTrue = JAVA_SET_HTTPONLY_TRUE_RE.test(code);
45237
45448
  for (const call of graph.ir.calls) {
45238
- const det = this.detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue);
45449
+ const det = this.detectJavaCookieCtor(call, code);
45239
45450
  if (!det) continue;
45240
45451
  insecureCookies.push(det);
45241
45452
  this.emit(ctx, file, det, "java");
@@ -45314,15 +45525,28 @@ var InsecureCookiePass = class {
45314
45525
  };
45315
45526
  }
45316
45527
  // ---------------- Java ----------------
45317
- 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) {
45318
45542
  const method = call.method_name ?? "";
45319
45543
  const isCookieCtor = method === "Cookie" || method.endsWith(".Cookie");
45320
45544
  if (!isCookieCtor) return null;
45321
45545
  const looksLikeCtor = call.is_constructor || !call.receiver && (call.receiver_type === "Cookie" || (call.receiver_type ?? "").endsWith(".Cookie")) || (call.resolution?.target ?? "").endsWith(".<init>");
45322
45546
  if (!looksLikeCtor) return null;
45323
45547
  if (call.arguments.length < 2) return null;
45324
- const missingSecure = !hasSetSecureTrue;
45325
- const missingHttpOnly = !hasSetHttpOnlyTrue;
45548
+ const missingSecure = !this.javaCookieSetterTrue(code, call, "setSecure");
45549
+ const missingHttpOnly = !this.javaCookieSetterTrue(code, call, "setHttpOnly");
45326
45550
  if (!missingSecure && !missingHttpOnly) return null;
45327
45551
  return {
45328
45552
  line: call.location.line,
@@ -49936,7 +50160,7 @@ async function analyzeForAPI(code, filePath, language, options = {}) {
49936
50160
  constPropResult.sanitizedVars,
49937
50161
  constPropResult.synchronizedLines
49938
50162
  );
49939
- filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
50163
+ filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls, taint.sources);
49940
50164
  let pythonTaintedVars = /* @__PURE__ */ new Map();
49941
50165
  if (language === "python") {
49942
50166
  pythonTaintedVars = buildPythonTaintedVars(code);
@@ -8080,6 +8080,17 @@ function extractGoCallInfo(node) {
8080
8080
  if (operand.type === "identifier") {
8081
8081
  const resolved = resolveGoLocalReceiverType(opText, node);
8082
8082
  receiver = resolved !== null ? resolved : opText;
8083
+ } else if (operand.type === "selector_expression" && methodName === "Get") {
8084
+ const fieldNode = operand.childForFieldName("field");
8085
+ const inner = operand.childForFieldName("operand");
8086
+ const fieldName = fieldNode ? getNodeText(fieldNode) : "";
8087
+ const innerName = inner && inner.type === "identifier" ? getNodeText(inner) : null;
8088
+ const innerType = innerName !== null ? resolveGoLocalReceiverType(innerName, node) : null;
8089
+ if (innerType === "Request" && (fieldName === "PostForm" || fieldName === "Form")) {
8090
+ receiver = fieldName === "PostForm" ? "RequestPostForm" : "RequestForm";
8091
+ } else {
8092
+ receiver = opText;
8093
+ }
8083
8094
  } else {
8084
8095
  receiver = opText;
8085
8096
  }
@@ -12448,8 +12459,15 @@ var DEFAULT_SINKS = [
12448
12459
  { method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
12449
12460
  { method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12450
12461
  { method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
12451
- // Wildfly/Undertow servlet resource manager
12452
- { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12462
+ // Wildfly/Undertow servlet resource manager.
12463
+ // ResourceManager / PathResourceManager / FileResourceManager /
12464
+ // ClassPathResourceManager are the interface and implementations WildFly
12465
+ // calls through (cognium-dev#622, CVE-2018-1047). Class-qualified only.
12466
+ { method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12467
+ { method: "getResource", class: "ResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12468
+ { method: "getResource", class: "PathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12469
+ { method: "getResource", class: "FileResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12470
+ { method: "getResource", class: "ClassPathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
12453
12471
  // Yamcs file system bucket
12454
12472
  { method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
12455
12473
  // RocketMQ validators
@@ -15320,7 +15338,8 @@ function isInterproceduralTaintableType(typeName, language) {
15320
15338
  }
15321
15339
  function isParameterizedQueryCall(call, pattern) {
15322
15340
  if (pattern.type !== "sql_injection") return false;
15323
- const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
15341
+ const positions = pattern.arg_positions ?? [];
15342
+ const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
15324
15343
  const queryArg = call.arguments.find((a) => a.position === queryPos);
15325
15344
  if (queryArg) {
15326
15345
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
@@ -17329,11 +17348,26 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17329
17348
  }
17330
17349
  const taintInfo = taintByDefId.get(use.def_id);
17331
17350
  if (taintInfo) {
17351
+ let sanMap = sanitizersByLine;
17352
+ if (identifierOutsideSanitizerCalls(
17353
+ use.variable,
17354
+ callsAtSink,
17355
+ sanitizersByLine.get(sink.line) ?? [],
17356
+ sink.type
17357
+ )) {
17358
+ const stripped = new Map(sanitizersByLine);
17359
+ const kept = (sanitizersByLine.get(sink.line) ?? []).filter(
17360
+ (s) => !sanitizerCoversSink(s, sink.type)
17361
+ );
17362
+ if (kept.length === 0) stripped.delete(sink.line);
17363
+ else stripped.set(sink.line, kept);
17364
+ sanMap = stripped;
17365
+ }
17332
17366
  const isSanitized = checkSanitized(
17333
17367
  taintInfo.line,
17334
17368
  sink.line,
17335
17369
  sink.type,
17336
- sanitizersByLine,
17370
+ sanMap,
17337
17371
  {
17338
17372
  startDefId: use.def_id,
17339
17373
  chainsByToDef: graph.chainsByToDef,
@@ -17487,6 +17521,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
17487
17521
  "crlf",
17488
17522
  "mass_assignment"
17489
17523
  ]);
17524
+ function readBalancedArg(text, openParen) {
17525
+ let depth = 0;
17526
+ let quote = null;
17527
+ let escaped = false;
17528
+ for (let i2 = openParen; i2 < text.length; i2++) {
17529
+ const c = text[i2];
17530
+ if (quote) {
17531
+ if (escaped) {
17532
+ escaped = false;
17533
+ continue;
17534
+ }
17535
+ if (c === "\\") {
17536
+ escaped = true;
17537
+ continue;
17538
+ }
17539
+ if (c === quote) quote = null;
17540
+ continue;
17541
+ }
17542
+ if (c === '"' || c === "'") {
17543
+ quote = c;
17544
+ continue;
17545
+ }
17546
+ if (c === "(") depth++;
17547
+ else if (c === ")") {
17548
+ depth--;
17549
+ if (depth === 0) return text.slice(openParen + 1, i2);
17550
+ }
17551
+ }
17552
+ return null;
17553
+ }
17554
+ function sanitizerCallRefs(sanitizers, sinkType) {
17555
+ const refs = [];
17556
+ for (const san of sanitizers) {
17557
+ if (!sanitizerCoversSink(san, sinkType)) continue;
17558
+ const m = /(?:(\w+)\.)?(\w+)\(\)/.exec(san.method);
17559
+ if (!m?.[2]) continue;
17560
+ refs.push({ receiver: m[1], methodName: m[2] });
17561
+ }
17562
+ return refs;
17563
+ }
17564
+ function maskSanitizerCalls(expr, refs) {
17565
+ let out2 = expr;
17566
+ for (const ref of refs) {
17567
+ const needles = ref.receiver ? [`${ref.receiver}.${ref.methodName}`, ref.methodName] : [ref.methodName];
17568
+ for (const needle of needles) {
17569
+ const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
17570
+ for (let guard = 0; guard < 8; guard++) {
17571
+ const m = re.exec(out2);
17572
+ if (!m) break;
17573
+ const open = m.index + m[0].length - 1;
17574
+ const arg = readBalancedArg(out2, open);
17575
+ if (arg === null) break;
17576
+ out2 = out2.slice(0, m.index) + " " + out2.slice(open + arg.length + 2);
17577
+ }
17578
+ }
17579
+ }
17580
+ return out2;
17581
+ }
17582
+ function blankStringLiterals(expr) {
17583
+ return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
17584
+ }
17585
+ function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType) {
17586
+ if (!/^[A-Za-z_$][\w$]*$/.test(variable)) return false;
17587
+ const refs = sanitizerCallRefs(sanitizers, sinkType);
17588
+ if (refs.length === 0) return false;
17589
+ const re = new RegExp(
17590
+ `(?:^|[^A-Za-z0-9_$])${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`
17591
+ );
17592
+ for (const call of calls) {
17593
+ if (refs.some((r) => r.methodName === call.method_name)) continue;
17594
+ for (const arg of call.arguments) {
17595
+ const expr = arg.expression ?? "";
17596
+ if (!expr) continue;
17597
+ const masked = maskSanitizerCalls(expr, refs);
17598
+ if (masked === expr) continue;
17599
+ if (re.test(blankStringLiterals(masked))) return true;
17600
+ }
17601
+ }
17602
+ return false;
17603
+ }
17490
17604
  function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
17491
17605
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
17492
17606
  const sanitizersAtTarget = sanitizersByLine.get(toLine);