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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +29 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/dependency-versions.d.ts +37 -0
- package/dist/analysis/dependency-versions.d.ts.map +1 -1
- package/dist/analysis/dependency-versions.js +70 -0
- package/dist/analysis/dependency-versions.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +18 -2
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts +47 -31
- package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts.map +1 -1
- package/dist/analysis/passes/python-receiver-taint-format-pass.js +84 -46
- package/dist/analysis/passes/python-receiver-taint-format-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +158 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +47 -11
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +18 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +247 -20
- package/dist/core/circle-ir-core.cjs +113 -8
- package/dist/core/circle-ir-core.js +113 -8
- package/dist/core/extractors/dfg.js +99 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- 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
|
|
6
|
-
*
|
|
7
|
-
* a taint that lives on the call's receiver —
|
|
8
|
-
* `str.format(*args)` shape puts the format template AS the
|
|
9
|
-
* not as an argument.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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
|
|
6
|
-
*
|
|
7
|
-
* a taint that lives on the call's receiver —
|
|
8
|
-
* `str.format(*args)` shape puts the format template AS the
|
|
9
|
-
* not as an argument.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
31
|
-
* synthetic-sink emission because the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
// set. Skip complex receivers
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// chain resolution
|
|
60
|
-
//
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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)
|
|
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
|
|
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;
|
|
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();
|