circle-ir 3.180.0 → 3.182.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.
Files changed (29) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +29 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/dependency-versions.d.ts +37 -0
  5. package/dist/analysis/dependency-versions.d.ts.map +1 -1
  6. package/dist/analysis/dependency-versions.js +70 -0
  7. package/dist/analysis/dependency-versions.js.map +1 -1
  8. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/language-sources-pass.js +18 -2
  10. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  11. package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts +47 -31
  12. package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts.map +1 -1
  13. package/dist/analysis/passes/python-receiver-taint-format-pass.js +84 -46
  14. package/dist/analysis/passes/python-receiver-taint-format-pass.js.map +1 -1
  15. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  16. package/dist/analysis/passes/taint-propagation-pass.js +158 -0
  17. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  18. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  19. package/dist/analysis/taint-propagation.js +47 -11
  20. package/dist/analysis/taint-propagation.js.map +1 -1
  21. package/dist/analyzer.d.ts +18 -0
  22. package/dist/analyzer.d.ts.map +1 -1
  23. package/dist/analyzer.js.map +1 -1
  24. package/dist/browser/circle-ir.js +247 -20
  25. package/dist/core/circle-ir-core.cjs +113 -8
  26. package/dist/core/circle-ir-core.js +113 -8
  27. package/dist/core/extractors/dfg.js +99 -0
  28. package/dist/core/extractors/dfg.js.map +1 -1
  29. package/package.json +1 -1
@@ -1,43 +1,59 @@
1
1
  /**
2
2
  * PythonReceiverTaintFormatPass — cognium-dev #264 (Python receiver-taint).
3
3
  *
4
- * Emits `format_string` (CWE-134) findings for Python `.format(...)`
5
- * calls whose RECEIVER is tainted. The pre-#264-Python-slice sink
6
- * patterns key exclusively on `arg_positions`, which cannot represent
7
- * a taint that lives on the call's receiver — Python's
8
- * `str.format(*args)` shape puts the format template AS the receiver,
9
- * not as an argument. Same is true of the `%`-operator shape
10
- * (`taintedFmt % args`), which lives entirely in a binary-operator
11
- * AST node, not a call.
12
- *
13
- * This pass takes the receiver-taint half of that gap by scanning
14
- * `graph.ir.calls` for `.format(...)` calls whose receiver appears in
15
- * the constant-propagation `tainted` set. On a hit, emits a direct
16
- * `format_string` finding at the call site (severity `medium`, level
17
- * `warning`).
18
- *
19
- * The `%`-operator half is genuinely bigger — needs a new AST scan
20
- * over `binary_operator` nodes with format-shape RHS detection — and
21
- * remains deferred on #264.
4
+ * Emits `format_string` (CWE-134) findings for Python `.format(...)` /
5
+ * `.format_map(...)` calls whose RECEIVER is a tainted variable. The
6
+ * generic sink-pattern schema keys exclusively on `arg_positions`,
7
+ * which cannot represent a taint that lives on the call's receiver —
8
+ * Python's `str.format(*args)` shape puts the format template AS the
9
+ * receiver, not as an argument.
10
+ *
11
+ * ## Coverage overlap with Sprint 86 (#189)
12
+ *
13
+ * `language-sources-pass.ts` already carries a Sprint 86 (#189)
14
+ * codepath that regex-matches `.format(` and `<name> %` shapes at
15
+ * file scope, gated on the presence of a Flask / Django / FastAPI
16
+ * request extractor in the source. **That path handles the common
17
+ * case.** This pass is intentionally a fallback for the shapes
18
+ * Sprint 86 misses:
19
+ *
20
+ * - Files that don't reference `request.args` / `flask.request`
21
+ * (Sprint 86 short-circuits and never runs).
22
+ * - `.format_map(...)` calls — Sprint 86's regex only accepts
23
+ * `.format\s*\(`, not `.format_map`. As of 3.181.0 Sprint 86's
24
+ * regex is extended, but this pass is kept as belt-and-suspenders.
25
+ * - Non-Flask taint sources (e.g. `os.environ`, `sys.stdin`) that
26
+ * the plugin's `getBuiltinSources()` catches but Sprint 86's
27
+ * framework-gate excludes.
28
+ *
29
+ * ## Taint signal
30
+ *
31
+ * The pass consumes `graph.ir.taint.sources` (populated by
32
+ * `TaintMatcherPass` before this pass runs). Each source has an
33
+ * optional `variable` field naming the identifier the source's
34
+ * return-value flows into. A `.format` call whose receiver name
35
+ * matches any source's `variable` name in the same file is
36
+ * treated as receiver-taint.
37
+ *
38
+ * (Prior to 3.181.0 this pass read `ConstantPropagatorResult.tainted`
39
+ * which is empty for many Python cases — the pass no-op'd whenever
40
+ * constant-propagation didn't populate the set. The switch to
41
+ * `graph.ir.taint.sources[].variable` matches the taint-tracker's
42
+ * own view of tainted identifiers and no longer depends on
43
+ * constant-propagation state.)
22
44
  *
23
45
  * Python-only. No-op on other languages.
24
46
  *
25
- * Pipeline slot: runs after `ConstantPropagationPass` so the
26
- * `tainted` set is populated. Any pass position after
27
- * `constant-propagation` is safe; placing near the sink-emission
28
- * chain keeps like-with-like grouped.
29
- *
30
- * Direct-finding emission (via `ctx.addFinding`) rather than
31
- * synthetic-sink emission because the format-string vulnerability
32
- * here doesn't need a taint-source → sink flow trace: the
33
- * `constant-propagation.tainted` set implicitly encodes that the
34
- * receiver traces back to a source. Emitting a sink would require
35
- * TaintPropagationPass to also understand receiver-taint, which
36
- * would be a much broader engine change.
47
+ * Direct-finding emission via `ctx.addFinding` rather than
48
+ * synthetic-sink emission because the source→sink relationship is
49
+ * already established: the taint source populated `variable` at
50
+ * assignment time; the format-call at the sink line consumes it
51
+ * directly. Emitting a sink would require TaintPropagationPass to
52
+ * also understand receiver-taint, a much broader engine change.
37
53
  */
38
54
  import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
39
55
  export interface PythonReceiverTaintFormatResult {
40
- /** Number of `.format()` calls flagged this run. */
56
+ /** Number of `.format()` / `.format_map()` calls flagged this run. */
41
57
  findingsEmitted: number;
42
58
  }
43
59
  export declare class PythonReceiverTaintFormatPass implements AnalysisPass<PythonReceiverTaintFormatResult> {
@@ -1 +1 @@
1
- {"version":3,"file":"python-receiver-taint-format-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,+BAA+B;IAC9C,oDAAoD;IACpD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CAsDvD"}
1
+ {"version":3,"file":"python-receiver-taint-format-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAE9E,MAAM,WAAW,+BAA+B;IAC9C,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CA6EvD"}
@@ -1,39 +1,55 @@
1
1
  /**
2
2
  * PythonReceiverTaintFormatPass — cognium-dev #264 (Python receiver-taint).
3
3
  *
4
- * Emits `format_string` (CWE-134) findings for Python `.format(...)`
5
- * calls whose RECEIVER is tainted. The pre-#264-Python-slice sink
6
- * patterns key exclusively on `arg_positions`, which cannot represent
7
- * a taint that lives on the call's receiver — Python's
8
- * `str.format(*args)` shape puts the format template AS the receiver,
9
- * not as an argument. Same is true of the `%`-operator shape
10
- * (`taintedFmt % args`), which lives entirely in a binary-operator
11
- * AST node, not a call.
4
+ * Emits `format_string` (CWE-134) findings for Python `.format(...)` /
5
+ * `.format_map(...)` calls whose RECEIVER is a tainted variable. The
6
+ * generic sink-pattern schema keys exclusively on `arg_positions`,
7
+ * which cannot represent a taint that lives on the call's receiver —
8
+ * Python's `str.format(*args)` shape puts the format template AS the
9
+ * receiver, not as an argument.
12
10
  *
13
- * This pass takes the receiver-taint half of that gap by scanning
14
- * `graph.ir.calls` for `.format(...)` calls whose receiver appears in
15
- * the constant-propagation `tainted` set. On a hit, emits a direct
16
- * `format_string` finding at the call site (severity `medium`, level
17
- * `warning`).
11
+ * ## Coverage overlap with Sprint 86 (#189)
18
12
  *
19
- * The `%`-operator half is genuinely bigger — needs a new AST scan
20
- * over `binary_operator` nodes with format-shape RHS detection and
21
- * remains deferred on #264.
13
+ * `language-sources-pass.ts` already carries a Sprint 86 (#189)
14
+ * codepath that regex-matches `.format(` and `<name> %` shapes at
15
+ * file scope, gated on the presence of a Flask / Django / FastAPI
16
+ * request extractor in the source. **That path handles the common
17
+ * case.** This pass is intentionally a fallback for the shapes
18
+ * Sprint 86 misses:
22
19
  *
23
- * Python-only. No-op on other languages.
20
+ * - Files that don't reference `request.args` / `flask.request`
21
+ * (Sprint 86 short-circuits and never runs).
22
+ * - `.format_map(...)` calls — Sprint 86's regex only accepts
23
+ * `.format\s*\(`, not `.format_map`. As of 3.181.0 Sprint 86's
24
+ * regex is extended, but this pass is kept as belt-and-suspenders.
25
+ * - Non-Flask taint sources (e.g. `os.environ`, `sys.stdin`) that
26
+ * the plugin's `getBuiltinSources()` catches but Sprint 86's
27
+ * framework-gate excludes.
28
+ *
29
+ * ## Taint signal
30
+ *
31
+ * The pass consumes `graph.ir.taint.sources` (populated by
32
+ * `TaintMatcherPass` before this pass runs). Each source has an
33
+ * optional `variable` field naming the identifier the source's
34
+ * return-value flows into. A `.format` call whose receiver name
35
+ * matches any source's `variable` name in the same file is
36
+ * treated as receiver-taint.
24
37
  *
25
- * Pipeline slot: runs after `ConstantPropagationPass` so the
26
- * `tainted` set is populated. Any pass position after
27
- * `constant-propagation` is safe; placing near the sink-emission
28
- * chain keeps like-with-like grouped.
38
+ * (Prior to 3.181.0 this pass read `ConstantPropagatorResult.tainted`
39
+ * which is empty for many Python cases — the pass no-op'd whenever
40
+ * constant-propagation didn't populate the set. The switch to
41
+ * `graph.ir.taint.sources[].variable` matches the taint-tracker's
42
+ * own view of tainted identifiers and no longer depends on
43
+ * constant-propagation state.)
44
+ *
45
+ * Python-only. No-op on other languages.
29
46
  *
30
- * Direct-finding emission (via `ctx.addFinding`) rather than
31
- * synthetic-sink emission because the format-string vulnerability
32
- * here doesn't need a taint-source sink flow trace: the
33
- * `constant-propagation.tainted` set implicitly encodes that the
34
- * receiver traces back to a source. Emitting a sink would require
35
- * TaintPropagationPass to also understand receiver-taint, which
36
- * would be a much broader engine change.
47
+ * Direct-finding emission via `ctx.addFinding` rather than
48
+ * synthetic-sink emission because the source→sink relationship is
49
+ * already established: the taint source populated `variable` at
50
+ * assignment time; the format-call at the sink line consumes it
51
+ * directly. Emitting a sink would require TaintPropagationPass to
52
+ * also understand receiver-taint, a much broader engine change.
37
53
  */
38
54
  export class PythonReceiverTaintFormatPass {
39
55
  name = 'python-receiver-taint-format';
@@ -42,33 +58,54 @@ export class PythonReceiverTaintFormatPass {
42
58
  if (ctx.language !== 'python') {
43
59
  return { findingsEmitted: 0 };
44
60
  }
45
- const { calls, meta } = ctx.graph.ir;
46
- const constProp = ctx.getResult('constant-propagation');
47
- const tainted = constProp.tainted;
61
+ const { calls, meta, taint } = ctx.graph.ir;
62
+ // Collect the set of variable names that hold tainted values,
63
+ // as seen by the taint-matcher (from `getBuiltinSources()`
64
+ // patterns with `return_tainted: true`). This is a smaller, more
65
+ // targeted signal than constant-propagation's `tainted` set —
66
+ // and, crucially, it's populated for Python files even when
67
+ // Sprint 86's Flask/Django/FastAPI regex gate short-circuits.
68
+ const taintedVarNames = new Set();
69
+ for (const s of taint.sources) {
70
+ if (typeof s.variable === 'string' && s.variable.length > 0) {
71
+ taintedVarNames.add(s.variable);
72
+ }
73
+ }
74
+ if (taintedVarNames.size === 0) {
75
+ return { findingsEmitted: 0 };
76
+ }
77
+ // Track (line, receiver) pairs we've already flagged this file
78
+ // to avoid emitting a second finding when Sprint 86 also fired
79
+ // on the same call — the two paths produce different `id` values
80
+ // but describe the same vulnerability at the same location.
81
+ const seen = new Set();
48
82
  let count = 0;
49
83
  for (const call of calls) {
50
- if (call.method_name !== 'format')
84
+ // `format` and `format_map` share the receiver-taint risk shape.
85
+ // Both put the format template as the receiver rather than an
86
+ // argument. `format_map` accepts a mapping instead of *args but
87
+ // the attribute-leak / DoS surface is identical.
88
+ if (call.method_name !== 'format' && call.method_name !== 'format_map')
51
89
  continue;
52
90
  const receiver = call.receiver;
53
91
  if (!receiver)
54
92
  continue;
55
- // Only bare-identifier receivers can be looked up in the tainted
56
- // set. Skip complex receivers (`obj.attr.format()`, `f(x).format()`)
57
- // those don't correspond to a variable name the taint tracker
58
- // would have on file. Recall may improve later with attribute-
59
- // chain resolution, but the direct-identifier form is the common
60
- // shape for user-controlled templates.
93
+ // Only bare-identifier receivers can be looked up in the
94
+ // tainted-source set. Skip complex receivers
95
+ // (`obj.attr.format()`, `f(x).format()`, `"literal".format()`)
96
+ // those either don't correspond to a taintable variable name
97
+ // or need attribute-chain resolution the taint tracker doesn't
98
+ // currently do.
61
99
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(receiver))
62
100
  continue;
63
- // constant-propagation stores both scoped (`method:var`) and
64
- // unscoped forms in the tainted set. Check both.
65
- const scopedName = call.in_method
66
- ? `${call.in_method}:${receiver}`
67
- : receiver;
68
- if (!tainted.has(receiver) && !tainted.has(scopedName))
101
+ if (!taintedVarNames.has(receiver))
102
+ continue;
103
+ const seenKey = `${call.location.line}:${receiver}`;
104
+ if (seen.has(seenKey))
69
105
  continue;
106
+ seen.add(seenKey);
70
107
  ctx.addFinding({
71
- id: `format_string-${meta.file}-${call.location.line}-${receiver}`,
108
+ id: `format_string-${meta.file}-${call.location.line}-${receiver}-${call.method_name}`,
72
109
  pass: this.name,
73
110
  category: 'security',
74
111
  rule_id: 'format_string',
@@ -76,7 +113,8 @@ export class PythonReceiverTaintFormatPass {
76
113
  severity: 'medium',
77
114
  level: 'warning',
78
115
  message: `Format-string vulnerability: the format template \`${receiver}\` ` +
79
- `is tainted (traces back to a taint source). Attacker can leak ` +
116
+ `is tainted (traces back to a taint source) and used as the ` +
117
+ `receiver of \`${call.method_name}(...)\`. Attacker can leak ` +
80
118
  `object attributes via \`{obj.__class__.__mro__[…]}\` chains or ` +
81
119
  `crash the process via unbounded specifiers. Use a literal ` +
82
120
  `format string and pass user input as an argument instead.`,
@@ -1 +1 @@
1
- {"version":3,"file":"python-receiver-taint-format-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAUH,MAAM,OAAO,6BAA6B;IAG/B,IAAI,GAAG,8BAA8B,CAAC;IACtC,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;QAChC,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAC7B,sBAAsB,CACvB,CAAC;QACF,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC;QAElC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,WAAW,KAAK,QAAQ;gBAAE,SAAS;YAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC/B,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAExB,iEAAiE;YACjE,qEAAqE;YACrE,gEAAgE;YAChE,+DAA+D;YAC/D,iEAAiE;YACjE,uCAAuC;YACvC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAEzD,6DAA6D;YAC7D,iDAAiD;YACjD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS;gBAC/B,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,QAAQ,EAAE;gBACjC,CAAC,CAAC,QAAQ,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,SAAS;YAEjE,GAAG,CAAC,UAAU,CAAC;gBACb,EAAE,EAAE,iBAAiB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE;gBAClE,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,UAAU;gBACpB,OAAO,EAAE,eAAe;gBACxB,GAAG,EAAE,SAAS;gBACd,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,SAAS;gBAChB,OAAO,EACL,sDAAsD,QAAQ,KAAK;oBACnE,gEAAgE;oBAChE,iEAAiE;oBACjE,4DAA4D;oBAC5D,2DAA2D;gBAC7D,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;aACzB,CAAC,CAAC;YACH,KAAK,EAAE,CAAC;QACV,CAAC;QAED,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;CACF"}
1
+ {"version":3,"file":"python-receiver-taint-format-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAU9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CAwXlD"}
1
+ {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAU9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CA8YlD"}
@@ -163,6 +163,21 @@ export class TaintPropagationPass {
163
163
  continue;
164
164
  pushIfNew(f);
165
165
  }
166
+ // Supplement: Go package-scope var flows (cognium-dev #243).
167
+ //
168
+ // Cross-function taint through package-level state that the per-function
169
+ // DFG cannot see. Text-scans the file for:
170
+ // - Package-scope var declarations at the top level
171
+ // - Assignments to those vars where the RHS is tainted (source or
172
+ // already-tainted derivative)
173
+ // - Sink calls anywhere that reference the same var name
174
+ // Emits one flow per (write-line, sink-line) pair. Language-gated to Go.
175
+ if (ctx.language === 'go' && typeof ctx.code === 'string') {
176
+ const goGlobalFlows = detectGoPackageGlobalFlows(ctx.code, calls, sources, sinks, constProp.unreachableLines) ?? [];
177
+ for (const f of goGlobalFlows) {
178
+ pushIfNew(f);
179
+ }
180
+ }
166
181
  // Sprint 9 #58.1 — final pass: drop any flow whose source variable was
167
182
  // explicitly marked sanitized by a guard (e.g. regex-allowlist).
168
183
  // Applied to ALL flow generators (DFG-built and the four supplements)
@@ -581,6 +596,149 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
581
596
  // ---------------------------------------------------------------------------
582
597
  // Helpers (moved verbatim from analyzer.ts)
583
598
  // ---------------------------------------------------------------------------
599
+ /**
600
+ * cognium-dev #243 — Go package-scope var flows.
601
+ *
602
+ * Detects the store-then-read shape where taint enters a package-level var
603
+ * in one function and is consumed by a sink in another function. The
604
+ * per-function DFG cannot link these because each function scope is
605
+ * independent; this text-scan supplement covers the common case.
606
+ *
607
+ * Heuristic (Go-specific):
608
+ * - Package-scope defs: top-level `var X ...` / `X = ...` at column 0.
609
+ * - Write: any `X = ...` (or `X = expr` inside a function) whose RHS
610
+ * contains an identifier that is either a taint source's variable or
611
+ * matches a direct source pattern (r.URL.Query()..., r.Header.Get..., …).
612
+ * - Read: any sink whose arg expression references `X` at word boundary.
613
+ *
614
+ * Bounded conservatism: does not fire when the local scope shadows the
615
+ * package var (short-var-decl `X := ...`) — that path stays local.
616
+ */
617
+ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLines) {
618
+ const flows = [];
619
+ if (!code || sinks.length === 0)
620
+ return flows;
621
+ const lines = code.split('\n');
622
+ const packageVars = new Set();
623
+ // Step 1: collect package-scope var names. Recognize:
624
+ // var X type
625
+ // var X = ...
626
+ // var ( X ... Y ... )
627
+ let inVarBlock = false;
628
+ for (const line of lines) {
629
+ const stripped = line.replace(/\s+$/, '');
630
+ if (!stripped)
631
+ continue;
632
+ // Only consider column-0 lines (package scope). Function bodies indent.
633
+ if (/^[ \t]/.test(line)) {
634
+ // Multi-line var block runs continue even indented, but the `var (`
635
+ // opener is at column 0 and we already flagged inVarBlock.
636
+ if (inVarBlock) {
637
+ const m = stripped.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+/);
638
+ if (m)
639
+ packageVars.add(m[1]);
640
+ if (/^\s*\)/.test(stripped))
641
+ inVarBlock = false;
642
+ }
643
+ continue;
644
+ }
645
+ if (/^var\s*\(/.test(stripped)) {
646
+ inVarBlock = true;
647
+ continue;
648
+ }
649
+ if (inVarBlock && /^\)/.test(stripped)) {
650
+ inVarBlock = false;
651
+ continue;
652
+ }
653
+ const varOne = stripped.match(/^var\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
654
+ if (varOne)
655
+ packageVars.add(varOne[1]);
656
+ }
657
+ if (packageVars.size === 0)
658
+ return flows;
659
+ // Step 2: for each package var, find writes with tainted RHS.
660
+ // A write is `X = ...` (assignment) — NOT `X := ...` (short-var-decl,
661
+ // which shadows the package var with a local).
662
+ const sourceVarNames = new Set();
663
+ for (const s of sources) {
664
+ if (typeof s.variable === 'string' && s.variable.length > 0) {
665
+ sourceVarNames.add(s.variable);
666
+ }
667
+ }
668
+ // Recognize a small set of direct Go source patterns on the RHS.
669
+ const GO_SOURCE_RE = /\b(r\.(?:URL\.Query|Form|PostForm|Header|Cookie|Body)|mux\.Vars|c\.(?:Query|Param|PostForm|GetHeader|Cookie)|ctx\.(?:Query|Param|PostForm|GetHeader|Cookie))\b/;
670
+ const writes = [];
671
+ for (let i = 0; i < lines.length; i++) {
672
+ const line = lines[i];
673
+ if (unreachableLines.has(i + 1))
674
+ continue;
675
+ // Skip short-var-decl to preserve local shadowing.
676
+ if (/:=/.test(line))
677
+ continue;
678
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
679
+ if (!m)
680
+ continue;
681
+ const [, lhs, rhs] = m;
682
+ if (!packageVars.has(lhs))
683
+ continue;
684
+ // RHS tainted if it references a source var or matches a direct pattern.
685
+ let taintedBy = null;
686
+ if (GO_SOURCE_RE.test(rhs)) {
687
+ const src = sources.find(s => s.line === i + 1);
688
+ taintedBy = src
689
+ ? { line: src.line, type: src.type }
690
+ : { line: i + 1, type: 'http_param' };
691
+ }
692
+ else {
693
+ for (const v of sourceVarNames) {
694
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_$])`);
695
+ if (re.test(rhs)) {
696
+ const src = sources.find(s => s.variable === v);
697
+ if (src) {
698
+ taintedBy = { line: src.line, type: src.type };
699
+ break;
700
+ }
701
+ }
702
+ }
703
+ }
704
+ if (!taintedBy)
705
+ continue;
706
+ writes.push({ varName: lhs, line: i + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
707
+ }
708
+ if (writes.length === 0)
709
+ return flows;
710
+ // Step 3: for each sink, if any arg expression references a written var,
711
+ // emit a flow from the source through the write to the sink.
712
+ for (const sink of sinks) {
713
+ if (unreachableLines.has(sink.line))
714
+ continue;
715
+ // Look at the sink line's text for identifier-boundary matches.
716
+ const sinkLineText = lines[sink.line - 1] ?? '';
717
+ if (!sinkLineText)
718
+ continue;
719
+ for (const w of writes) {
720
+ if (w.line >= sink.line)
721
+ continue; // write must precede sink lexically
722
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_$])`);
723
+ if (!re.test(sinkLineText))
724
+ continue;
725
+ flows.push({
726
+ source_line: w.sourceLine,
727
+ sink_line: sink.line,
728
+ source_type: w.sourceType,
729
+ sink_type: sink.type,
730
+ path: [
731
+ { variable: w.varName, line: w.sourceLine, type: 'source' },
732
+ { variable: w.varName, line: w.line, type: 'assignment' },
733
+ { variable: w.varName, line: sink.line, type: 'sink' },
734
+ ],
735
+ confidence: 0.9,
736
+ sanitized: false,
737
+ });
738
+ }
739
+ }
740
+ return flows;
741
+ }
584
742
  function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
585
743
  const flows = [];
586
744
  const callsByLine = new Map();