circle-ir 4.14.2 → 4.15.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/const-table-lookup.d.ts +29 -0
- package/dist/analysis/const-table-lookup.d.ts.map +1 -0
- package/dist/analysis/const-table-lookup.js +145 -0
- package/dist/analysis/const-table-lookup.js.map +1 -0
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +15 -4
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/go-strings-replacer.d.ts +21 -0
- package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
- package/dist/analysis/go-strings-replacer.js +145 -0
- package/dist/analysis/go-strings-replacer.js.map +1 -0
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +53 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-canonical-prefix.d.ts +42 -0
- package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
- package/dist/analysis/java-canonical-prefix.js +145 -0
- package/dist/analysis/java-canonical-prefix.js.map +1 -0
- package/dist/analysis/java-crlf-strip.d.ts +30 -0
- package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/java-crlf-strip.js +195 -0
- package/dist/analysis/java-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
- package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +43 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +20 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +842 -10
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +162 -6
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +207 -1
- package/dist/analysis/passes/sink-filter-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 +257 -2
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/scalar-derivation.d.ts +25 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -0
- package/dist/analysis/scalar-derivation.js +196 -0
- package/dist/analysis/scalar-derivation.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +167 -6
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +149 -19
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +7 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +3140 -1186
- package/dist/core/circle-ir-core.cjs +485 -27
- package/dist/core/circle-ir-core.js +485 -27
- package/dist/core/extractors/dfg.js +240 -6
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/graph/code-graph.d.ts +9 -0
- package/dist/graph/code-graph.d.ts.map +1 -1
- package/dist/graph/code-graph.js +11 -0
- package/dist/graph/code-graph.js.map +1 -1
- package/dist/resolution/cross-file.d.ts +7 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +10 -1
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +3 -3
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Depends on: sink-filter, constant-propagation
|
|
12
12
|
*/
|
|
13
|
+
import { findJavaSpringReturnXSSSinks } from './language-sources-pass.js';
|
|
13
14
|
import { identifierOutsideSanitizerCalls, propagateTaint } from '../taint-propagation.js';
|
|
14
15
|
import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
|
|
15
16
|
import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, findPythonTrustBoundaryViolations, findPythonReturnXSSSinks } from './language-sources-pass.js';
|
|
16
17
|
import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
|
|
17
18
|
import { walkBackwardDefs } from '../dfg-walk.js';
|
|
18
19
|
import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
20
|
+
import { javaCanonicalStartsWithAllBare } from '../java-canonical-prefix.js';
|
|
19
21
|
/**
|
|
20
22
|
* cognium-dev#328 — the const-prop per-step veto, with two scoped exemptions.
|
|
21
23
|
*
|
|
@@ -40,6 +42,177 @@ import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
|
40
42
|
* code, resolved constant) are unaffected.
|
|
41
43
|
*/
|
|
42
44
|
const CONTAINER_READ_SOURCES = new Set(['plugin_param', 'config_param']);
|
|
45
|
+
/**
|
|
46
|
+
* #515 — find the real source a Python derived variable actually references.
|
|
47
|
+
* Reads the derivation's right-hand side at its origin line (f-string braces
|
|
48
|
+
* kept, plain quotes and trailing `#…` comments dropped), picks the real
|
|
49
|
+
* source variables named there (earliest line wins when several), and follows
|
|
50
|
+
* names that are themselves derived — including a later assignment of a name
|
|
51
|
+
* that was also a real source. Returns undefined when nothing can be
|
|
52
|
+
* resolved, so the caller keeps its earliest-source fallback.
|
|
53
|
+
*/
|
|
54
|
+
/** Drop a trailing `#…` comment, leaving `#` inside string literals alone. */
|
|
55
|
+
function stripPythonTrailingComment(text) {
|
|
56
|
+
let i = 0;
|
|
57
|
+
while (i < text.length) {
|
|
58
|
+
const ch = text[i];
|
|
59
|
+
if (ch === '"' || ch === "'") {
|
|
60
|
+
const triple = text.startsWith(ch.repeat(3), i);
|
|
61
|
+
const delim = triple ? ch.repeat(3) : ch;
|
|
62
|
+
i += delim.length;
|
|
63
|
+
while (i < text.length && !text.startsWith(delim, i)) {
|
|
64
|
+
if (text[i] === '\\') {
|
|
65
|
+
i += 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
i += delim.length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '#')
|
|
74
|
+
return text.slice(0, i);
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Keep only the f-string interpolation expression: cut before a top-level
|
|
81
|
+
* `!conversion` or `:format_spec` (Python replacement_field grammar). A `!`
|
|
82
|
+
* followed by `=` is the `!=` operator and is not a cut point. Depth
|
|
83
|
+
* tracks `()[]{}`; string literals are skipped so a `:` inside a subscript
|
|
84
|
+
* key does not truncate the expression.
|
|
85
|
+
*/
|
|
86
|
+
function pythonFStringNestedFields(body) {
|
|
87
|
+
// Format-spec / conversion tails only: a `{` here always opens a nested
|
|
88
|
+
// replacement field (Python 3.12+). Do NOT treat `{{` / `}}` as literal-brace
|
|
89
|
+
// escapes — `f"{label:{{fmt}}}"` parses as a nested FormattedValue whose
|
|
90
|
+
// expression is the set `{fmt}`. The top-level f-string skip stays in
|
|
91
|
+
// braceExprs so a literal `f"{{fmt}}"` still does not credit `fmt`. Each
|
|
92
|
+
// nested `{…}` is itself an expression (and may carry its own conversion /
|
|
93
|
+
// format-spec), so recurse through pythonFStringExprOnly.
|
|
94
|
+
let acc = '';
|
|
95
|
+
for (let j = 0; j < body.length;) {
|
|
96
|
+
if (body[j] === '{') {
|
|
97
|
+
j++;
|
|
98
|
+
let depth = 1;
|
|
99
|
+
let inner = '';
|
|
100
|
+
while (j < body.length && depth > 0) {
|
|
101
|
+
if (body[j] === '{')
|
|
102
|
+
depth++;
|
|
103
|
+
else if (body[j] === '}') {
|
|
104
|
+
depth--;
|
|
105
|
+
if (depth === 0)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
inner += body[j];
|
|
109
|
+
j++;
|
|
110
|
+
}
|
|
111
|
+
j++;
|
|
112
|
+
acc += ' ' + pythonFStringExprOnly(inner) + ' ';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
j++;
|
|
116
|
+
}
|
|
117
|
+
return acc;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Keep the expression part of an f-string replacement field; drop literal
|
|
121
|
+
* conversion / format-spec text, but retain nested `{…}` fields from the
|
|
122
|
+
* discarded tail (format specs may contain nested replacement fields).
|
|
123
|
+
*/
|
|
124
|
+
function pythonFStringExprOnly(inner) {
|
|
125
|
+
let depth = 0;
|
|
126
|
+
let i = 0;
|
|
127
|
+
while (i < inner.length) {
|
|
128
|
+
const ch = inner[i];
|
|
129
|
+
if (ch === '"' || ch === "'") {
|
|
130
|
+
const triple = inner.startsWith(ch.repeat(3), i);
|
|
131
|
+
const delim = triple ? ch.repeat(3) : ch;
|
|
132
|
+
i += delim.length;
|
|
133
|
+
while (i < inner.length && !inner.startsWith(delim, i)) {
|
|
134
|
+
if (inner[i] === '\\') {
|
|
135
|
+
i += 2;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
i += delim.length;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
144
|
+
depth++;
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
149
|
+
depth--;
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (depth === 0 && ch === '!') {
|
|
154
|
+
// `!=` is the comparison operator, not a `!conversion`; keep scanning.
|
|
155
|
+
let j = i + 1;
|
|
156
|
+
while (j < inner.length && (inner[j] === ' ' || inner[j] === '\t'))
|
|
157
|
+
j++;
|
|
158
|
+
if (inner[j] === '=') {
|
|
159
|
+
i = j + 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const expr = inner.slice(0, i);
|
|
163
|
+
const nested = pythonFStringNestedFields(inner.slice(i + 1));
|
|
164
|
+
return nested ? expr + nested : expr;
|
|
165
|
+
}
|
|
166
|
+
if (depth === 0 && ch === ':') {
|
|
167
|
+
const expr = inner.slice(0, i);
|
|
168
|
+
const nested = pythonFStringNestedFields(inner.slice(i + 1));
|
|
169
|
+
return nested ? expr + nested : expr;
|
|
170
|
+
}
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
return inner;
|
|
174
|
+
}
|
|
175
|
+
function resolvePythonDerivedAnchor(varName, derived, realSourceByVar, codeLines, visiting) {
|
|
176
|
+
if (visiting.has(varName))
|
|
177
|
+
return undefined;
|
|
178
|
+
visiting.add(varName);
|
|
179
|
+
const originLine = derived.get(varName);
|
|
180
|
+
if (originLine === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
const text = codeLines[originLine - 1];
|
|
183
|
+
if (!text)
|
|
184
|
+
return undefined;
|
|
185
|
+
const eq = text.search(/(?<![=!<>+\-*/%&|^:])=(?!=)/);
|
|
186
|
+
if (eq < 0)
|
|
187
|
+
return undefined;
|
|
188
|
+
// Drop a real trailing `#…` comment first (outside strings only), then lift
|
|
189
|
+
// f-string `{…}` names. Format specs / conversion flags are cut inside
|
|
190
|
+
// braceExprs so a `#` in `{width:#04x}` cannot eat later names, and `!s`
|
|
191
|
+
// cannot become a scanned identifier; nested `{…}` fields in the format
|
|
192
|
+
// spec (e.g. `{label:{fmt}}`) are kept.
|
|
193
|
+
const rhs = maskStringLiteralsForTokenScan(stripPythonTrailingComment(text.slice(eq + 1)));
|
|
194
|
+
let best;
|
|
195
|
+
for (const m of rhs.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
|
|
196
|
+
const id = m[0];
|
|
197
|
+
if (id === varName)
|
|
198
|
+
continue;
|
|
199
|
+
const real = realSourceByVar.get(id);
|
|
200
|
+
const derivedLine = derived.get(id);
|
|
201
|
+
// A later assignment of the same name (recorded in `derived` after the
|
|
202
|
+
// real source line) is the value that flows — resolve it first. Fall
|
|
203
|
+
// back to the real source when that derivation does not resolve (e.g.
|
|
204
|
+
// `fmt = request.form.get("fmt")`, whose RHS names no source variable).
|
|
205
|
+
let cand;
|
|
206
|
+
if (derivedLine !== undefined && (!real || derivedLine > real.line)) {
|
|
207
|
+
cand = resolvePythonDerivedAnchor(id, derived, realSourceByVar, codeLines, visiting);
|
|
208
|
+
}
|
|
209
|
+
if (!cand)
|
|
210
|
+
cand = real;
|
|
211
|
+
if (cand && (!best || cand.line < best.line))
|
|
212
|
+
best = cand;
|
|
213
|
+
}
|
|
214
|
+
return best;
|
|
215
|
+
}
|
|
43
216
|
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
44
217
|
const lines = typeof code === 'string' ? code.split('\n') : [];
|
|
45
218
|
const selfReassign = new Map();
|
|
@@ -186,6 +359,38 @@ export class TaintPropagationPass {
|
|
|
186
359
|
flows.push(f);
|
|
187
360
|
return true;
|
|
188
361
|
};
|
|
362
|
+
// Supplement: Spring MVC return-as-body XSS flows — cognium-dev#566. The
|
|
363
|
+
// sink `LanguageSourcesPass` synthesises at the `return` has no method or
|
|
364
|
+
// argument, so no argument matcher reaches it; the detector already knows
|
|
365
|
+
// both ends, so the flow is emitted here (as for Python #368 below).
|
|
366
|
+
if (ctx.language === 'java' && typeof ctx.code === 'string') {
|
|
367
|
+
const xssSanitizerLines = sanitizers
|
|
368
|
+
.filter(sa => sa.sanitizes.includes('xss'))
|
|
369
|
+
.map(sa => sa.line);
|
|
370
|
+
for (const rx of findJavaSpringReturnXSSSinks(ctx.code, types, sources, xssSanitizerLines)) {
|
|
371
|
+
if (constProp.unreachableLines.has(rx.sinkLine))
|
|
372
|
+
continue;
|
|
373
|
+
const sink = sinks.find(sk => sk.line === rx.sinkLine && sk.type === 'xss');
|
|
374
|
+
if (!sink)
|
|
375
|
+
continue;
|
|
376
|
+
const atLine = sources.filter(sc => sc.line === rx.sourceLine);
|
|
377
|
+
const src = atLine.find(sc => sc.type !== 'interprocedural_param') ?? atLine[0];
|
|
378
|
+
if (!src)
|
|
379
|
+
continue;
|
|
380
|
+
pushIfNew({
|
|
381
|
+
source_line: src.line,
|
|
382
|
+
sink_line: rx.sinkLine,
|
|
383
|
+
source_type: src.type,
|
|
384
|
+
sink_type: 'xss',
|
|
385
|
+
path: [
|
|
386
|
+
{ variable: src.variable ?? 'response', line: src.line, type: 'source' },
|
|
387
|
+
{ variable: src.variable ?? 'response', line: rx.sinkLine, type: 'sink' },
|
|
388
|
+
],
|
|
389
|
+
confidence: (src.confidence ?? 1) * (sink.confidence ?? 1),
|
|
390
|
+
sanitized: false,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
189
394
|
// Supplement: Python trust-boundary (CWE-501) flows — cognium-dev#363.
|
|
190
395
|
//
|
|
191
396
|
// `LanguageSourcesPass` detects `session[<tainted>] = …` and synthesises a
|
|
@@ -421,6 +626,24 @@ export class TaintPropagationPass {
|
|
|
421
626
|
arr.push(san);
|
|
422
627
|
sanitizersByLine.set(san.line, arr);
|
|
423
628
|
}
|
|
629
|
+
// True unless every def at `line` among `visited` is provably off this
|
|
630
|
+
// flow: not a variable on its path, and a complete backward walk from it
|
|
631
|
+
// never reaches `sourceLine`.
|
|
632
|
+
const sanitizedDefOnFlow = (visited, line, f) => {
|
|
633
|
+
let sawDef = false;
|
|
634
|
+
for (const id of visited) {
|
|
635
|
+
const def = graph.defById.get(id);
|
|
636
|
+
if (!def || def.line !== line)
|
|
637
|
+
continue;
|
|
638
|
+
sawDef = true;
|
|
639
|
+
if (f.path.some(step => step.variable === def.variable))
|
|
640
|
+
return true;
|
|
641
|
+
const back = walkBackwardDefs(id, graph.chainsByToDef, graph.defById, { maxHops: 32 });
|
|
642
|
+
if (back.hopCapReached || back.lines.has(f.source_line))
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
return !sawDef;
|
|
646
|
+
};
|
|
424
647
|
finalFlows = finalFlows.filter(f => {
|
|
425
648
|
// Two-tier filter:
|
|
426
649
|
// - `external_taint_escape` (synthetic CWE-668 fallback): no
|
|
@@ -507,6 +730,12 @@ export class TaintPropagationPass {
|
|
|
507
730
|
const sansAtLine = sanitizersByLine.get(line);
|
|
508
731
|
if (!sansAtLine || sansAtLine.length === 0)
|
|
509
732
|
continue;
|
|
733
|
+
// cognium-dev#348: the walk visits every contributor of the sink
|
|
734
|
+
// value. In `clean = a.replaceAll(..); msg = clean + b; log(msg)`
|
|
735
|
+
// the strip on `a` must not credit the flow from `b`. Skip a
|
|
736
|
+
// sanitized def that does not derive from this flow's source.
|
|
737
|
+
if (line > f.source_line && !sanitizedDefOnFlow(walk.visited, line, f))
|
|
738
|
+
continue;
|
|
510
739
|
for (const san of sansAtLine) {
|
|
511
740
|
if (sanitizerCoversSink(san, f.sink_type)) {
|
|
512
741
|
return false;
|
|
@@ -829,6 +1058,10 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
|
|
|
829
1058
|
return false;
|
|
830
1059
|
if (!/\.startsWith\s*\([^)]*getCanonicalPath/.test(body))
|
|
831
1060
|
return false;
|
|
1061
|
+
// cognium-dev#618: `startsWith(parent.getCanonicalPath())` with no
|
|
1062
|
+
// trailing separator is the prefix-confusion bypass, not a guard.
|
|
1063
|
+
if (javaCanonicalStartsWithAllBare(body))
|
|
1064
|
+
return false;
|
|
832
1065
|
if (!/\bthrow\s+new\b/.test(body))
|
|
833
1066
|
return false;
|
|
834
1067
|
return true;
|
|
@@ -1599,9 +1832,18 @@ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachabl
|
|
|
1599
1832
|
}
|
|
1600
1833
|
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
1601
1834
|
const literalOnly = new RegExp(`^${strLit}$`);
|
|
1835
|
+
// For an if/else chain the literal's own arm is skipped: an earlier
|
|
1836
|
+
// assignment in that same arm (`if (a) { if (b) x = input; x = "safe"; }`) is
|
|
1837
|
+
// overwritten by the literal, it is not a sibling branch. A switch body shares
|
|
1838
|
+
// one brace, so every line of it stays a sibling.
|
|
1839
|
+
const isSwitch = /\bswitch\b/.test(block.header);
|
|
1840
|
+
const ownStart = block.line;
|
|
1841
|
+
const ownEnd = end.line;
|
|
1602
1842
|
for (let i = first; i <= last; i++) {
|
|
1603
1843
|
if (i === idx || unreachableLines?.has(i + 1))
|
|
1604
1844
|
continue;
|
|
1845
|
+
if (!isSwitch && i > ownStart && i < ownEnd)
|
|
1846
|
+
continue;
|
|
1605
1847
|
const m = assign.exec(lines[i]);
|
|
1606
1848
|
if (m && m[1] && !literalOnly.test(m[1]))
|
|
1607
1849
|
return true;
|
|
@@ -1692,7 +1934,7 @@ export function maskStringLiteralsForTokenScan(expr) {
|
|
|
1692
1934
|
j++;
|
|
1693
1935
|
}
|
|
1694
1936
|
j++;
|
|
1695
|
-
acc += ' ' + maskStringLiteralsForTokenScan(inner) + ' ';
|
|
1937
|
+
acc += ' ' + maskStringLiteralsForTokenScan(pythonFStringExprOnly(inner)) + ' ';
|
|
1696
1938
|
continue;
|
|
1697
1939
|
}
|
|
1698
1940
|
j++;
|
|
@@ -1885,6 +2127,15 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1885
2127
|
anchor = s;
|
|
1886
2128
|
}
|
|
1887
2129
|
}
|
|
2130
|
+
// #515: real (non-derived) sources by variable name, earliest first, and
|
|
2131
|
+
// the file's lines for reading each derivation's right-hand side.
|
|
2132
|
+
const realSourceByVar = new Map();
|
|
2133
|
+
for (const s of sourcesWithVar) {
|
|
2134
|
+
const prev = realSourceByVar.get(s.variable);
|
|
2135
|
+
if (!prev || s.line < prev.line)
|
|
2136
|
+
realSourceByVar.set(s.variable, s);
|
|
2137
|
+
}
|
|
2138
|
+
const codeLines = code.split('\n');
|
|
1888
2139
|
for (const [varName, originLine] of derived) {
|
|
1889
2140
|
if (!varName || existingVars.has(varName))
|
|
1890
2141
|
continue;
|
|
@@ -1894,8 +2145,12 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1894
2145
|
// `http_param` type — the for-loop / bare-inline patterns we want
|
|
1895
2146
|
// to recover here are all web-request-derived in practice.
|
|
1896
2147
|
if (hasRealSource && anchor) {
|
|
2148
|
+
// #515: anchor to the real source this derivation actually
|
|
2149
|
+
// references (through earlier derived vars); the earliest-source
|
|
2150
|
+
// anchor stays the fallback when none can be found.
|
|
2151
|
+
const referenced = resolvePythonDerivedAnchor(varName, derived, realSourceByVar, codeLines, new Set());
|
|
1897
2152
|
sourcesWithVar.push({
|
|
1898
|
-
...anchor,
|
|
2153
|
+
...(referenced ?? anchor),
|
|
1899
2154
|
variable: varName,
|
|
1900
2155
|
});
|
|
1901
2156
|
}
|