circle-ir 3.72.1 → 3.74.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.
@@ -12502,11 +12502,21 @@ var DEFAULT_SANITIZERS = [
12502
12502
  // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
12503
12503
  // stricter Clean+HasPrefix guard recognition is tracked separately).
12504
12504
  // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
12505
- { method: "Base", class: "filepath", removes: ["path_traversal"] },
12506
- { method: "Base", class: "path", removes: ["path_traversal"] },
12507
- { method: "Clean", class: "filepath", removes: ["path_traversal"] },
12508
- { method: "Clean", class: "path", removes: ["path_traversal"] },
12509
- { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
12505
+ // Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
12506
+ // fallback so canonicalised paths don't trigger the synthetic sink.
12507
+ { method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
12508
+ { method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
12509
+ { method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
12510
+ { method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
12511
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
12512
+ // Go html/template escape helpers (#102 FP-27) — registered explicitly because
12513
+ // configs/sinks/golang.json is not loaded at runtime.
12514
+ { method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
12515
+ { method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
12516
+ { method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
12517
+ { method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
12518
+ { method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
12519
+ { method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
12510
12520
  // Log Injection sanitizers
12511
12521
  { method: "replace", removes: ["log_injection"] },
12512
12522
  // Used to remove newlines/control chars
@@ -12985,6 +12995,15 @@ function findSources(calls, types, patterns, sourceLines, language) {
12985
12995
  s.variable = m[1];
12986
12996
  }
12987
12997
  }
12998
+ if (language === "go" && sourceLines) {
12999
+ const GO_ASSIGN_LHS = /^\s*(?:var\s+)?([A-Za-z_]\w*)(?:\s*,\s*[A-Za-z_]\w*)*\s*(?::\s*[A-Za-z_][\w.]*\s*)?(?::?=)(?!=)/;
13000
+ for (const s of result) {
13001
+ if (s.variable && s.variable.length > 0) continue;
13002
+ const lineText = sourceLines[s.line - 1] ?? "";
13003
+ const m = GO_ASSIGN_LHS.exec(lineText);
13004
+ if (m) s.variable = m[1];
13005
+ }
13006
+ }
12988
13007
  return result;
12989
13008
  }
12990
13009
  function isInterproceduralTaintableType(typeName) {
@@ -13104,6 +13123,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
13104
13123
  }
13105
13124
  return true;
13106
13125
  }
13126
+ function isSafeGoExecCommandCall(call, pattern, language) {
13127
+ if (language !== "go") return false;
13128
+ if (pattern.type !== "command_injection") return false;
13129
+ if (pattern.class !== "exec") return false;
13130
+ if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
13131
+ const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
13132
+ const programArg = call.arguments.find((a) => a.position === programArgPos);
13133
+ if (!programArg) return false;
13134
+ let program;
13135
+ if (programArg.literal !== null && programArg.literal !== void 0) {
13136
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
13137
+ } else {
13138
+ const expr = (programArg.expression ?? "").trim();
13139
+ if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
13140
+ return false;
13141
+ }
13142
+ const stripped = expr.slice(1, -1);
13143
+ program = stripped.split("/").pop() ?? stripped;
13144
+ }
13145
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
13146
+ "sh",
13147
+ "bash",
13148
+ "zsh",
13149
+ "dash",
13150
+ "ash",
13151
+ "ksh",
13152
+ "cmd",
13153
+ "cmd.exe",
13154
+ "powershell",
13155
+ "pwsh",
13156
+ "powershell.exe",
13157
+ "pwsh.exe"
13158
+ ]);
13159
+ if (SHELL_PROGRAMS.has(program)) return false;
13160
+ return true;
13161
+ }
13107
13162
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
13108
13163
  function argIsClassLiteral(call, position) {
13109
13164
  const arg = call.arguments.find((a) => a.position === position);
@@ -13123,6 +13178,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13123
13178
  if (isSafePythonSubprocessCall(call, pattern, language)) {
13124
13179
  continue;
13125
13180
  }
13181
+ if (isSafeGoExecCommandCall(call, pattern, language)) {
13182
+ continue;
13183
+ }
13126
13184
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
13127
13185
  continue;
13128
13186
  }
@@ -15544,8 +15602,32 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
15544
15602
  "BufferedWriter",
15545
15603
  "PrintStream",
15546
15604
  "PrintWriter",
15547
- "ObjectOutputStream"
15605
+ "ObjectOutputStream",
15548
15606
  // ObjectInputStream IS a sink (deserialization), keep it out
15607
+ // Go database/sql query methods — when parameterised (?, $N, :name),
15608
+ // these are safe boundaries; the sql_injection check governs the unsafe
15609
+ // shape. Without this, parameterised queries fall through to
15610
+ // external_taint_escape. (cognium-dev #102 FP-19a)
15611
+ "Query",
15612
+ "QueryRow",
15613
+ "QueryContext",
15614
+ "QueryRowContext",
15615
+ "Exec",
15616
+ "ExecContext",
15617
+ // Go html/template escape helpers — explicit safe utilities.
15618
+ // Belt-and-suspenders with the sanitizer config so external_taint_escape
15619
+ // is suppressed regardless of class-prefix matching. (cognium-dev #102 FP-27)
15620
+ "EscapeString",
15621
+ "HTMLEscapeString",
15622
+ "JSEscapeString",
15623
+ "URLQueryEscaper",
15624
+ // Go os/exec — when isSafeGoExecCommandCall has already cleared the
15625
+ // command_injection sink (fixed program literal, non-shell), the call
15626
+ // should not fall through to external_taint_escape on the variadic args.
15627
+ // Genuine command_injection is still emitted via the configured sink.
15628
+ // (cognium-dev #102 FP-25)
15629
+ "Command",
15630
+ "CommandContext"
15549
15631
  ]);
15550
15632
  const sanitizerMethods = /* @__PURE__ */ new Set();
15551
15633
  for (const san of sanitizers) {
@@ -21326,14 +21408,19 @@ var GoPlugin = class extends BaseLanguagePlugin {
21326
21408
  severity: "critical",
21327
21409
  argPositions: [0]
21328
21410
  },
21329
- // Command Injection
21411
+ // Command Injection — argPositions intentionally empty so all variadic
21412
+ // positions are scanned. `exec.Command("sh", "-c", taintedCmd)` puts the
21413
+ // injection at arg[2]; `exec.Command(taintedName, args...)` puts it at
21414
+ // arg[0]; `exec.Command("git", taintedFlag)` is argument-injection at
21415
+ // arg[1] (CWE-88). All three shapes are dangerous when any positional
21416
+ // arg is tainted. (cognium-dev #53)
21330
21417
  {
21331
21418
  method: "Command",
21332
21419
  class: "exec",
21333
21420
  type: "command_injection",
21334
21421
  cwe: "CWE-78",
21335
21422
  severity: "critical",
21336
- argPositions: [0]
21423
+ argPositions: []
21337
21424
  },
21338
21425
  {
21339
21426
  method: "CommandContext",
@@ -21341,7 +21428,7 @@ var GoPlugin = class extends BaseLanguagePlugin {
21341
21428
  type: "command_injection",
21342
21429
  cwe: "CWE-78",
21343
21430
  severity: "critical",
21344
- argPositions: [1]
21431
+ argPositions: []
21345
21432
  },
21346
21433
  // Path Traversal
21347
21434
  {
@@ -21436,6 +21523,117 @@ var GoPlugin = class extends BaseLanguagePlugin {
21436
21523
  cwe: "CWE-502",
21437
21524
  severity: "medium",
21438
21525
  argPositions: [0]
21526
+ },
21527
+ // Log Injection — `log.Printf("event=%s", userInput)` allows
21528
+ // CRLF/log forging when the format string interpolates user data.
21529
+ // CWE-117. (cognium-dev #107)
21530
+ {
21531
+ method: "Print",
21532
+ class: "log",
21533
+ type: "log_injection",
21534
+ cwe: "CWE-117",
21535
+ severity: "medium",
21536
+ argPositions: []
21537
+ },
21538
+ {
21539
+ method: "Println",
21540
+ class: "log",
21541
+ type: "log_injection",
21542
+ cwe: "CWE-117",
21543
+ severity: "medium",
21544
+ argPositions: []
21545
+ },
21546
+ {
21547
+ method: "Printf",
21548
+ class: "log",
21549
+ type: "log_injection",
21550
+ cwe: "CWE-117",
21551
+ severity: "medium",
21552
+ argPositions: []
21553
+ },
21554
+ {
21555
+ method: "Fatal",
21556
+ class: "log",
21557
+ type: "log_injection",
21558
+ cwe: "CWE-117",
21559
+ severity: "medium",
21560
+ argPositions: []
21561
+ },
21562
+ {
21563
+ method: "Fatalln",
21564
+ class: "log",
21565
+ type: "log_injection",
21566
+ cwe: "CWE-117",
21567
+ severity: "medium",
21568
+ argPositions: []
21569
+ },
21570
+ {
21571
+ method: "Fatalf",
21572
+ class: "log",
21573
+ type: "log_injection",
21574
+ cwe: "CWE-117",
21575
+ severity: "medium",
21576
+ argPositions: []
21577
+ },
21578
+ {
21579
+ method: "Panic",
21580
+ class: "log",
21581
+ type: "log_injection",
21582
+ cwe: "CWE-117",
21583
+ severity: "medium",
21584
+ argPositions: []
21585
+ },
21586
+ {
21587
+ method: "Panicln",
21588
+ class: "log",
21589
+ type: "log_injection",
21590
+ cwe: "CWE-117",
21591
+ severity: "medium",
21592
+ argPositions: []
21593
+ },
21594
+ {
21595
+ method: "Panicf",
21596
+ class: "log",
21597
+ type: "log_injection",
21598
+ cwe: "CWE-117",
21599
+ severity: "medium",
21600
+ argPositions: []
21601
+ },
21602
+ // Server-Side Template Injection — `text/template` and `html/template`
21603
+ // template parse-time injection. When the *template source itself*
21604
+ // is tainted (not the data), the rendered output can execute
21605
+ // arbitrary template directives. CWE-94. (cognium-dev #108)
21606
+ {
21607
+ method: "Parse",
21608
+ class: "Template",
21609
+ type: "code_injection",
21610
+ cwe: "CWE-94",
21611
+ severity: "high",
21612
+ argPositions: [0]
21613
+ },
21614
+ {
21615
+ method: "ParseFiles",
21616
+ class: "template",
21617
+ type: "code_injection",
21618
+ cwe: "CWE-94",
21619
+ severity: "high",
21620
+ argPositions: []
21621
+ },
21622
+ {
21623
+ method: "ParseGlob",
21624
+ class: "template",
21625
+ type: "code_injection",
21626
+ cwe: "CWE-94",
21627
+ severity: "high",
21628
+ argPositions: [0]
21629
+ },
21630
+ {
21631
+ method: "ParseFS",
21632
+ class: "template",
21633
+ type: "code_injection",
21634
+ cwe: "CWE-94",
21635
+ severity: "high",
21636
+ argPositions: []
21439
21637
  }
21440
21638
  ];
21441
21639
  }
@@ -22498,6 +22696,11 @@ var LanguageSourcesPass = class {
22498
22696
  ctx.addFinding(finding);
22499
22697
  }
22500
22698
  additionalSanitizers.push(...findBashRegexAllowlistSanitizers(code));
22699
+ additionalSanitizers.push(...findBashRealpathPrefixGuardSanitizers(code));
22700
+ }
22701
+ if (language === "go") {
22702
+ additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
22703
+ additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
22501
22704
  }
22502
22705
  attachSourceLineCode(additionalSources, additionalSinks, code);
22503
22706
  return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
@@ -23361,6 +23564,140 @@ function isSafeBashAllowlistRegex(literal) {
23361
23564
  }
23362
23565
  return consumed === body2.length;
23363
23566
  }
23567
+ function findBashRealpathPrefixGuardSanitizers(code) {
23568
+ const sanitizers = [];
23569
+ const lines = code.split("\n");
23570
+ const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
23571
+ const esacClose = /^\s*esac\b/;
23572
+ const armOpener = /^\s*([^)\s][^)]*?)\)/;
23573
+ const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
23574
+ const catchAllArm = /^(?:\*|\\\*)$/;
23575
+ let i2 = 0;
23576
+ while (i2 < lines.length) {
23577
+ if (!caseOpen.test(lines[i2])) {
23578
+ i2++;
23579
+ continue;
23580
+ }
23581
+ let caseEnd = -1;
23582
+ for (let j = i2 + 1; j < lines.length; j++) {
23583
+ if (esacClose.test(lines[j])) {
23584
+ caseEnd = j;
23585
+ break;
23586
+ }
23587
+ }
23588
+ if (caseEnd === -1) {
23589
+ i2++;
23590
+ continue;
23591
+ }
23592
+ let hasPrefixArm = false;
23593
+ let hasTerminalCatchAll = false;
23594
+ for (let j = i2 + 1; j < caseEnd; j++) {
23595
+ const armMatch = armOpener.exec(lines[j]);
23596
+ if (!armMatch) continue;
23597
+ const pattern = armMatch[1].trim();
23598
+ if (catchAllArm.test(pattern)) {
23599
+ let bodyEnd = caseEnd;
23600
+ for (let k = j + 1; k < caseEnd; k++) {
23601
+ if (armOpener.test(lines[k])) {
23602
+ bodyEnd = k;
23603
+ break;
23604
+ }
23605
+ }
23606
+ const armBody = lines.slice(j, bodyEnd).join(" ");
23607
+ if (/\b(exit|return|die)\b/.test(armBody)) {
23608
+ hasTerminalCatchAll = true;
23609
+ }
23610
+ } else if (prefixArm.test(pattern)) {
23611
+ hasPrefixArm = true;
23612
+ }
23613
+ }
23614
+ if (hasPrefixArm && hasTerminalCatchAll) {
23615
+ for (let l = i2 + 1; l <= caseEnd + 1; l++) {
23616
+ sanitizers.push({
23617
+ type: "realpath_prefix_guard",
23618
+ method: "case",
23619
+ line: l,
23620
+ sanitizes: [
23621
+ "path_traversal",
23622
+ "command_injection",
23623
+ "code_injection",
23624
+ "ssrf",
23625
+ "open_redirect",
23626
+ "log_injection"
23627
+ ]
23628
+ });
23629
+ }
23630
+ }
23631
+ i2 = caseEnd + 1;
23632
+ }
23633
+ return sanitizers;
23634
+ }
23635
+ function findGoMapAllowlistGuardSanitizers(code) {
23636
+ const sanitizers = [];
23637
+ const lines = code.split("\n");
23638
+ const guardOpen = /^\s*if\s+!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\[\s*[A-Za-z_][A-Za-z0-9_]*\s*\]\s*\{/;
23639
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
23640
+ for (let i2 = 0; i2 < lines.length; i2++) {
23641
+ const m = guardOpen.exec(lines[i2]);
23642
+ if (!m) continue;
23643
+ const mapName = m[1];
23644
+ if (!allowlistName.test(mapName)) continue;
23645
+ let depth = 1;
23646
+ let closeLine = -1;
23647
+ let bodyHasTerminator = false;
23648
+ const maxScan = Math.min(lines.length, i2 + 26);
23649
+ for (let j = i2 + 1; j < maxScan; j++) {
23650
+ const line = lines[j];
23651
+ if (/\b(return|panic\s*\(|os\.Exit\s*\()/.test(line)) {
23652
+ bodyHasTerminator = true;
23653
+ }
23654
+ for (const ch of line) {
23655
+ if (ch === "{") depth++;
23656
+ else if (ch === "}") depth--;
23657
+ }
23658
+ if (depth === 0) {
23659
+ closeLine = j;
23660
+ break;
23661
+ }
23662
+ }
23663
+ if (closeLine === -1 || !bodyHasTerminator) continue;
23664
+ for (let l = closeLine + 2; l <= lines.length; l++) {
23665
+ sanitizers.push({
23666
+ type: "go_map_allowlist_guard",
23667
+ method: "if",
23668
+ line: l,
23669
+ sanitizes: [
23670
+ "ssrf",
23671
+ "open_redirect",
23672
+ "path_traversal",
23673
+ "sql_injection",
23674
+ "command_injection",
23675
+ "external_taint_escape"
23676
+ ]
23677
+ });
23678
+ }
23679
+ }
23680
+ return sanitizers;
23681
+ }
23682
+ function findGoHtmlTemplateImportSanitizers(code) {
23683
+ const sanitizers = [];
23684
+ const hasHtmlTemplate = /["\s]html\/template["\s]/.test(code);
23685
+ const hasTextTemplate = /["\s]text\/template["\s]/.test(code);
23686
+ if (!hasHtmlTemplate) return sanitizers;
23687
+ if (hasTextTemplate) return sanitizers;
23688
+ const lines = code.split("\n");
23689
+ const execCall = /\.(Execute|ExecuteTemplate)\s*\(/;
23690
+ for (let i2 = 0; i2 < lines.length; i2++) {
23691
+ if (!execCall.test(lines[i2])) continue;
23692
+ sanitizers.push({
23693
+ type: "html_template_auto_escape",
23694
+ method: "Execute",
23695
+ line: i2 + 1,
23696
+ sanitizes: ["xss", "external_taint_escape", "open_redirect"]
23697
+ });
23698
+ }
23699
+ return sanitizers;
23700
+ }
23364
23701
 
23365
23702
  // src/analysis/passes/sink-filter-pass.ts
23366
23703
  var JS_XSS_SANITIZERS = [
@@ -23858,6 +24195,38 @@ var TaintPropagationPass = class {
23858
24195
  }
23859
24196
  return true;
23860
24197
  });
24198
+ if (sanitizers && sanitizers.length > 0) {
24199
+ const sanitizersByLine = /* @__PURE__ */ new Map();
24200
+ for (const san of sanitizers) {
24201
+ const arr = sanitizersByLine.get(san.line) ?? [];
24202
+ arr.push(san);
24203
+ sanitizersByLine.set(san.line, arr);
24204
+ }
24205
+ finalFlows = finalFlows.filter((f) => {
24206
+ if (f.sink_type === "external_taint_escape") {
24207
+ const lo = Math.min(f.source_line, f.sink_line);
24208
+ const hi = Math.max(f.source_line, f.sink_line);
24209
+ for (let line = lo; line <= hi; line++) {
24210
+ const sansAtLine = sanitizersByLine.get(line);
24211
+ if (!sansAtLine) continue;
24212
+ for (const san of sansAtLine) {
24213
+ if (san.sanitizes.includes(f.sink_type)) {
24214
+ return false;
24215
+ }
24216
+ }
24217
+ }
24218
+ return true;
24219
+ }
24220
+ const sansAtSink = sanitizersByLine.get(f.sink_line);
24221
+ if (!sansAtSink || sansAtSink.length === 0) return true;
24222
+ for (const san of sansAtSink) {
24223
+ if (san.sanitizes.includes(f.sink_type)) {
24224
+ return false;
24225
+ }
24226
+ }
24227
+ return true;
24228
+ });
24229
+ }
23861
24230
  if (ctx.language === "java" && typeof ctx.code === "string") {
23862
24231
  finalFlows = finalFlows.filter((f) => {
23863
24232
  if (f.sink_type !== "path_traversal" && f.sink_type !== "xxe") return true;
@@ -24549,7 +24918,47 @@ var InterproceduralPass = class {
24549
24918
  if (additionalSinks.length > 0) {
24550
24919
  attachSourceLineCode([], additionalSinks, ctx.code);
24551
24920
  }
24552
- return { additionalSinks, additionalFlows, interprocedural };
24921
+ let filteredAdditionalFlows = additionalFlows;
24922
+ let filteredAdditionalSinks = additionalSinks;
24923
+ if (sanitizers && sanitizers.length > 0) {
24924
+ const sanitizersByLine = /* @__PURE__ */ new Map();
24925
+ for (const san of sanitizers) {
24926
+ const arr = sanitizersByLine.get(san.line) ?? [];
24927
+ arr.push(san);
24928
+ sanitizersByLine.set(san.line, arr);
24929
+ }
24930
+ const sanitizedSinkKeys = /* @__PURE__ */ new Set();
24931
+ filteredAdditionalFlows = additionalFlows.filter((f) => {
24932
+ if (f.sink_type === "external_taint_escape") {
24933
+ const lo = Math.min(f.source_line, f.sink_line);
24934
+ const hi = Math.max(f.source_line, f.sink_line);
24935
+ for (let line = lo; line <= hi; line++) {
24936
+ const sansAtLine = sanitizersByLine.get(line);
24937
+ if (!sansAtLine) continue;
24938
+ for (const san of sansAtLine) {
24939
+ if (san.sanitizes.includes(f.sink_type)) {
24940
+ sanitizedSinkKeys.add(`${f.sink_line}:${f.sink_type}`);
24941
+ return false;
24942
+ }
24943
+ }
24944
+ }
24945
+ return true;
24946
+ }
24947
+ const sansAtSink = sanitizersByLine.get(f.sink_line);
24948
+ if (!sansAtSink || sansAtSink.length === 0) return true;
24949
+ for (const san of sansAtSink) {
24950
+ if (san.sanitizes.includes(f.sink_type)) {
24951
+ return false;
24952
+ }
24953
+ }
24954
+ return true;
24955
+ });
24956
+ filteredAdditionalSinks = additionalSinks.filter((s) => {
24957
+ if (s.type !== "external_taint_escape") return true;
24958
+ return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
24959
+ });
24960
+ }
24961
+ return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
24553
24962
  }
24554
24963
  };
24555
24964
 
@@ -11884,11 +11884,21 @@ var DEFAULT_SANITIZERS = [
11884
11884
  // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
11885
11885
  // stricter Clean+HasPrefix guard recognition is tracked separately).
11886
11886
  // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
11887
- { method: "Base", class: "filepath", removes: ["path_traversal"] },
11888
- { method: "Base", class: "path", removes: ["path_traversal"] },
11889
- { method: "Clean", class: "filepath", removes: ["path_traversal"] },
11890
- { method: "Clean", class: "path", removes: ["path_traversal"] },
11891
- { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
11887
+ // Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
11888
+ // fallback so canonicalised paths don't trigger the synthetic sink.
11889
+ { method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11890
+ { method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11891
+ { method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11892
+ { method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11893
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11894
+ // Go html/template escape helpers (#102 FP-27) — registered explicitly because
11895
+ // configs/sinks/golang.json is not loaded at runtime.
11896
+ { method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11897
+ { method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11898
+ { method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
11899
+ { method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
11900
+ { method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11901
+ { method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11892
11902
  // Log Injection sanitizers
11893
11903
  { method: "replace", removes: ["log_injection"] },
11894
11904
  // Used to remove newlines/control chars
@@ -12280,6 +12290,15 @@ function findSources(calls, types, patterns, sourceLines, language) {
12280
12290
  s.variable = m[1];
12281
12291
  }
12282
12292
  }
12293
+ if (language === "go" && sourceLines) {
12294
+ const GO_ASSIGN_LHS = /^\s*(?:var\s+)?([A-Za-z_]\w*)(?:\s*,\s*[A-Za-z_]\w*)*\s*(?::\s*[A-Za-z_][\w.]*\s*)?(?::?=)(?!=)/;
12295
+ for (const s of result) {
12296
+ if (s.variable && s.variable.length > 0) continue;
12297
+ const lineText = sourceLines[s.line - 1] ?? "";
12298
+ const m = GO_ASSIGN_LHS.exec(lineText);
12299
+ if (m) s.variable = m[1];
12300
+ }
12301
+ }
12283
12302
  return result;
12284
12303
  }
12285
12304
  function isInterproceduralTaintableType(typeName) {
@@ -12399,6 +12418,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
12399
12418
  }
12400
12419
  return true;
12401
12420
  }
12421
+ function isSafeGoExecCommandCall(call, pattern, language) {
12422
+ if (language !== "go") return false;
12423
+ if (pattern.type !== "command_injection") return false;
12424
+ if (pattern.class !== "exec") return false;
12425
+ if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
12426
+ const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
12427
+ const programArg = call.arguments.find((a) => a.position === programArgPos);
12428
+ if (!programArg) return false;
12429
+ let program;
12430
+ if (programArg.literal !== null && programArg.literal !== void 0) {
12431
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12432
+ } else {
12433
+ const expr = (programArg.expression ?? "").trim();
12434
+ if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
12435
+ return false;
12436
+ }
12437
+ const stripped = expr.slice(1, -1);
12438
+ program = stripped.split("/").pop() ?? stripped;
12439
+ }
12440
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
12441
+ "sh",
12442
+ "bash",
12443
+ "zsh",
12444
+ "dash",
12445
+ "ash",
12446
+ "ksh",
12447
+ "cmd",
12448
+ "cmd.exe",
12449
+ "powershell",
12450
+ "pwsh",
12451
+ "powershell.exe",
12452
+ "pwsh.exe"
12453
+ ]);
12454
+ if (SHELL_PROGRAMS.has(program)) return false;
12455
+ return true;
12456
+ }
12402
12457
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12403
12458
  function argIsClassLiteral(call, position) {
12404
12459
  const arg = call.arguments.find((a) => a.position === position);
@@ -12418,6 +12473,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12418
12473
  if (isSafePythonSubprocessCall(call, pattern, language)) {
12419
12474
  continue;
12420
12475
  }
12476
+ if (isSafeGoExecCommandCall(call, pattern, language)) {
12477
+ continue;
12478
+ }
12421
12479
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12422
12480
  continue;
12423
12481
  }