circle-ir 4.9.27 → 4.9.29
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 +58 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +130 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +63 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +162 -8
- package/dist/analysis/passes/language-sources-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 +214 -16
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.d.ts.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.js +15 -0
- package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +62 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +486 -33
- package/dist/core/circle-ir-core.cjs +173 -13
- package/dist/core/circle-ir-core.js +173 -13
- package/dist/core/extractors/calls.js +155 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/types.js +50 -7
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/types/config.d.ts +14 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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;AA0FD,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,CA8hBhD;CACF;AA6sBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6FnE"}
|
|
@@ -16,6 +16,97 @@ import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, fin
|
|
|
16
16
|
import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
|
|
17
17
|
import { walkBackwardDefs } from '../dfg-walk.js';
|
|
18
18
|
import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
19
|
+
/**
|
|
20
|
+
* cognium-dev#328 — the const-prop per-step veto, with two scoped exemptions.
|
|
21
|
+
*
|
|
22
|
+
* `isFalsePositive`'s `variable_not_tainted` reason discards a flow when
|
|
23
|
+
* const-prop tracked the step variable (value `unknown`) but never marked it
|
|
24
|
+
* tainted. On Java that verdict is usually informed — const-prop does its own
|
|
25
|
+
* key-aware collection / safe-overwrite reasoning, and lifting the veto
|
|
26
|
+
* globally measured +1 on-category TP vs +311 FP files on OWASP. But const-prop
|
|
27
|
+
* only seeds taint from its own narrow pattern list, so a variable fed by a
|
|
28
|
+
* parameter or by a registered source it has no pattern for is "untainted"
|
|
29
|
+
* purely for lack of information.
|
|
30
|
+
*
|
|
31
|
+
* Walking the step variable's reaching-def chain back (through self-derived
|
|
32
|
+
* reassignments only), the veto is skipped when:
|
|
33
|
+
* (A) the chain crosses at least one `x = f(x)` / `x op= y` and ends inside
|
|
34
|
+
* the source..sink span — the #287 shape (`v = v.Trim()`); or
|
|
35
|
+
* (E) the chain ends at the source's own assignment line, and that source is
|
|
36
|
+
* not a container read (`plugin_param` / `config_param`), where const-prop
|
|
37
|
+
* is key-aware and its verdict is informed.
|
|
38
|
+
* Reaching defs are per method, so a same-named variable in a sibling method
|
|
39
|
+
* (Juliet `*_41` / `*_45` G2B sinks) never qualifies. Reasons 1 and 2 (dead
|
|
40
|
+
* code, resolved constant) are unaffected.
|
|
41
|
+
*/
|
|
42
|
+
const CONTAINER_READ_SOURCES = new Set(['plugin_param', 'config_param']);
|
|
43
|
+
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
44
|
+
const lines = typeof code === 'string' ? code.split('\n') : [];
|
|
45
|
+
const selfReassign = new Map();
|
|
46
|
+
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
for (const [line, defs] of graph.defsByLine) {
|
|
48
|
+
const text = lines[line - 1];
|
|
49
|
+
if (!text)
|
|
50
|
+
continue;
|
|
51
|
+
for (const d of defs) {
|
|
52
|
+
if (d.kind !== 'local' && d.kind !== 'param')
|
|
53
|
+
continue;
|
|
54
|
+
const v = escape(d.variable);
|
|
55
|
+
// `v op= …` or `v = …v…` (RHS reads the variable being assigned).
|
|
56
|
+
const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
|
|
57
|
+
if (!m)
|
|
58
|
+
continue;
|
|
59
|
+
if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
|
|
60
|
+
const arr = selfReassign.get(d.variable) ?? [];
|
|
61
|
+
arr.push(line);
|
|
62
|
+
selfReassign.set(d.variable, arr);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
|
|
67
|
+
/** Reaching def of `v` as read on `line` (a def on that line wins: it is the value after the line). */
|
|
68
|
+
const defAt = (v, line) => {
|
|
69
|
+
const own = (graph.defsByLine.get(line) ?? []).find(d => d.variable === v);
|
|
70
|
+
if (own)
|
|
71
|
+
return own;
|
|
72
|
+
const use = (graph.usesByLine.get(line) ?? []).find(u => u.variable === v && u.def_id != null);
|
|
73
|
+
return use ? graph.defById.get(use.def_id) : undefined;
|
|
74
|
+
};
|
|
75
|
+
return (step, sourceLine, sinkLine) => {
|
|
76
|
+
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
77
|
+
if (!r.isFalsePositive)
|
|
78
|
+
return false;
|
|
79
|
+
if (r.reason !== 'variable_not_tainted')
|
|
80
|
+
return true;
|
|
81
|
+
const lo = Math.min(sourceLine, sinkLine);
|
|
82
|
+
// Walk the reaching-def chain back from this step, stepping through
|
|
83
|
+
// self-derived reassignments only. Reaching defs are per method, so a
|
|
84
|
+
// same-named variable in a sibling method can never be picked up.
|
|
85
|
+
let d = defAt(step.variable, step.line);
|
|
86
|
+
let selfHops = 0;
|
|
87
|
+
for (let guard = 0; d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
|
|
88
|
+
const here = d.id;
|
|
89
|
+
const prior = (graph.usesByLine.get(d.line) ?? [])
|
|
90
|
+
.find(u => u.variable === d.variable && u.def_id != null && u.def_id !== here);
|
|
91
|
+
if (!prior)
|
|
92
|
+
break;
|
|
93
|
+
selfHops++;
|
|
94
|
+
d = graph.defById.get(prior.def_id);
|
|
95
|
+
}
|
|
96
|
+
if (!d)
|
|
97
|
+
return true;
|
|
98
|
+
// (E) the chain ends at the source's own assignment, so const-prop's
|
|
99
|
+
// "untainted" can only mean it has no pattern for this source. Container
|
|
100
|
+
// reads are excluded: const-prop tracks collection keys, and there its
|
|
101
|
+
// verdict is informed (`bar = map.get("safeKey")` — 10 OWASP FP files).
|
|
102
|
+
if (d.line === sourceLine && sources.some(s => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type)))
|
|
103
|
+
return false;
|
|
104
|
+
// (A) the chain crossed at least one `x = f(x)` and ends inside the span.
|
|
105
|
+
if (selfHops > 0 && d.line >= lo)
|
|
106
|
+
return false;
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
19
110
|
export class TaintPropagationPass {
|
|
20
111
|
name = 'taint-propagation';
|
|
21
112
|
category = 'security';
|
|
@@ -37,13 +128,15 @@ export class TaintPropagationPass {
|
|
|
37
128
|
}
|
|
38
129
|
// DFG-based taint propagation
|
|
39
130
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
131
|
+
// cognium-dev#328 — scoped exemption from const-prop's `variable_not_tainted`
|
|
132
|
+
// veto for self-derived reassignment (`x = f(x)`, `x += y`), the #287 shape.
|
|
133
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
40
134
|
// Filter flows: eliminate dead-code paths and constant-propagation FPs
|
|
41
135
|
const verifiedFlows = propagationResult.flows.filter(flow => {
|
|
42
136
|
if (constProp.unreachableLines.has(flow.sink.line))
|
|
43
137
|
return false;
|
|
44
138
|
for (const step of flow.path) {
|
|
45
|
-
|
|
46
|
-
if (fpCheck.isFalsePositive)
|
|
139
|
+
if (isVetoed(step, flow.source.line, flow.sink.line))
|
|
47
140
|
return false;
|
|
48
141
|
}
|
|
49
142
|
if (isCorrelatedPredicateFP(constProp, flow))
|
|
@@ -216,7 +309,7 @@ export class TaintPropagationPass {
|
|
|
216
309
|
continue;
|
|
217
310
|
let isFP = false;
|
|
218
311
|
for (const step of f.path) {
|
|
219
|
-
if (
|
|
312
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
220
313
|
isFP = true;
|
|
221
314
|
break;
|
|
222
315
|
}
|
|
@@ -261,7 +354,7 @@ export class TaintPropagationPass {
|
|
|
261
354
|
continue;
|
|
262
355
|
let isFP = false;
|
|
263
356
|
for (const step of f.path) {
|
|
264
|
-
if (
|
|
357
|
+
if (isVetoed(step, f.source_line, f.sink_line)) {
|
|
265
358
|
isFP = true;
|
|
266
359
|
break;
|
|
267
360
|
}
|
|
@@ -1403,19 +1496,40 @@ export function maskStringLiteralsForTokenScan(expr) {
|
|
|
1403
1496
|
}
|
|
1404
1497
|
return out;
|
|
1405
1498
|
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Does `expr` reference one of `sources`' variables in a CODE position?
|
|
1501
|
+
*
|
|
1502
|
+
* String-literal bodies are stripped first, so a tainted variable NAME that
|
|
1503
|
+
* merely appears as text inside the SQL or markup — `id` in
|
|
1504
|
+
* `"… WHERE id=@id"`, which is the parameterized, safe form — does not count.
|
|
1505
|
+
* Interpolation holes are kept, because `$"… {id}"` really does splice the
|
|
1506
|
+
* value in. Shared by the #271 CommandText seeding and the #530 builder
|
|
1507
|
+
* seeding, which need exactly the same judgement.
|
|
1508
|
+
*/
|
|
1509
|
+
function argsReferenceTaintedVar(expr, sources) {
|
|
1510
|
+
const codePart = expr
|
|
1511
|
+
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
|
|
1512
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
1513
|
+
// Comments are not code. A trailing `// pass `id` through` would otherwise
|
|
1514
|
+
// seed taint on the strength of prose. Literals are stripped first, so a
|
|
1515
|
+
// `//` inside a string (a URL, say) is already gone by this point.
|
|
1516
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
1517
|
+
.replace(/\/\/[^\n]*/g, ' ');
|
|
1518
|
+
return sources.some(s => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'u').test(codePart));
|
|
1519
|
+
}
|
|
1406
1520
|
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
|
|
1407
1521
|
const flows = [];
|
|
1408
1522
|
// #472 — Go function ranges for the same-function gate below. Returns the
|
|
1409
1523
|
// start line of the outermost function containing `line`, or -1 when the
|
|
1410
1524
|
// line is at package level (no scoping then, so package-level sources still
|
|
1411
1525
|
// reach every function).
|
|
1412
|
-
const
|
|
1526
|
+
const methodRanges = (langs) => language && langs.includes(language) && types
|
|
1413
1527
|
? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line]))
|
|
1414
1528
|
: [];
|
|
1415
|
-
const
|
|
1529
|
+
const enclosingStart = (ranges, line) => {
|
|
1416
1530
|
let best = -1;
|
|
1417
1531
|
let bestSpan = -1;
|
|
1418
|
-
for (const [start, end] of
|
|
1532
|
+
for (const [start, end] of ranges) {
|
|
1419
1533
|
if (line >= start && line <= end && end - start > bestSpan) {
|
|
1420
1534
|
best = start;
|
|
1421
1535
|
bestSpan = end - start;
|
|
@@ -1423,6 +1537,14 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1423
1537
|
}
|
|
1424
1538
|
return best;
|
|
1425
1539
|
};
|
|
1540
|
+
const goFnRanges = methodRanges(['go']);
|
|
1541
|
+
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
1542
|
+
// cognium-dev#530 — the same gate for C#, used by the builder seeding below.
|
|
1543
|
+
// Available only since #539 made C# method ranges actually populate; before
|
|
1544
|
+
// that `types[].methods` was empty for any file with a `#if` directive, which
|
|
1545
|
+
// is every Juliet C# file.
|
|
1546
|
+
const csFnRanges = methodRanges(['csharp']);
|
|
1547
|
+
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
1426
1548
|
// Variable-name scan path: only consider sources that carry an explicit
|
|
1427
1549
|
// variable name. The colocation path below (cognium-dev #83) runs even
|
|
1428
1550
|
// when this set is empty, so we no longer early-return.
|
|
@@ -1902,21 +2024,79 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1902
2024
|
const [, obj, rhs] = m;
|
|
1903
2025
|
if (known.has(obj))
|
|
1904
2026
|
continue;
|
|
1905
|
-
|
|
1906
|
-
// drop plain string-literal bodies — so a tainted var NAME appearing as
|
|
1907
|
-
// a substring inside the SQL text (e.g. `id` in `"… WHERE id=@id"`, a
|
|
1908
|
-
// parameterized/safe query) does NOT spuriously taint the command.
|
|
1909
|
-
const codePart = rhs
|
|
1910
|
-
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
|
|
1911
|
-
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ');
|
|
1912
|
-
const rhsTainted = sourcesWithVar.some(s => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'u').test(codePart));
|
|
1913
|
-
if (!rhsTainted)
|
|
2027
|
+
if (!argsReferenceTaintedVar(rhs, sourcesWithVar))
|
|
1914
2028
|
continue;
|
|
1915
2029
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i + 1 });
|
|
1916
2030
|
added = true;
|
|
1917
2031
|
}
|
|
1918
2032
|
}
|
|
1919
2033
|
}
|
|
2034
|
+
// Keys (`variable@line`) of sources synthesised by the #530 builder seeding
|
|
2035
|
+
// below. These need a tighter match rule than a real source: see the gate in
|
|
2036
|
+
// the candidate loop.
|
|
2037
|
+
const builderSeeds = new Set();
|
|
2038
|
+
// cognium-dev#530: builder-carried taint. `sb.Append(<tainted>)` leaves the
|
|
2039
|
+
// taint inside the StringBuilder, and `sb.ToString()` reads it back out — so
|
|
2040
|
+
// without this, a statement accumulated across several calls is invisible
|
|
2041
|
+
// while the same statement concatenated in one expression is caught. That is
|
|
2042
|
+
// not a corner case: a StringBuilder is the idiomatic way to build anything
|
|
2043
|
+
// multi-line in C#, and NIST Juliet's entire CWE-94 family uses it, which is
|
|
2044
|
+
// why that family scored 0.0% for want of propagation rather than a sink.
|
|
2045
|
+
//
|
|
2046
|
+
// Same shape as #271 above: taint enters an object, is read back later, so it
|
|
2047
|
+
// has to ride the object. Seeding the receiver as a tainted var at the append
|
|
2048
|
+
// line lets the existing argument scan link `sb.ToString()` to its sink.
|
|
2049
|
+
//
|
|
2050
|
+
// Receiver-gated to variables declared as a StringBuilder. `Insert` and
|
|
2051
|
+
// `Append` are far too common as names to match on a bare receiver — gating
|
|
2052
|
+
// on the declared type is what keeps this from tainting arbitrary objects.
|
|
2053
|
+
if (language === 'csharp' && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
2054
|
+
const lines = code.split('\n');
|
|
2055
|
+
// `StringBuilder sb` covers locals, fields and parameters; the second form
|
|
2056
|
+
// covers `var sb = new StringBuilder(...)`, where the type is on the right.
|
|
2057
|
+
const builders = new Set();
|
|
2058
|
+
for (const line of lines) {
|
|
2059
|
+
for (const m of line.matchAll(/\bStringBuilder\s+([\p{L}_][\p{L}\p{N}_]*)/gu))
|
|
2060
|
+
builders.add(m[1]);
|
|
2061
|
+
for (const m of line.matchAll(/\b([\p{L}_][\p{L}\p{N}_]*)\s*=\s*new\s+StringBuilder\b/gu))
|
|
2062
|
+
builders.add(m[1]);
|
|
2063
|
+
}
|
|
2064
|
+
if (builders.size > 0) {
|
|
2065
|
+
// `Append`, `AppendLine`, `AppendFormat` and `Insert` all write through to
|
|
2066
|
+
// the builder's contents. `Replace` is deliberately absent: it can just as
|
|
2067
|
+
// easily be removing the tainted text as adding it, so crediting it either
|
|
2068
|
+
// way would be a guess.
|
|
2069
|
+
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
2070
|
+
const anchor = sourcesWithVar.reduce((a, s) => (s.line < a.line ? s : a), sourcesWithVar[0]);
|
|
2071
|
+
let added = true;
|
|
2072
|
+
let guard = 0;
|
|
2073
|
+
while (added && guard < lines.length + 2) {
|
|
2074
|
+
added = false;
|
|
2075
|
+
guard++;
|
|
2076
|
+
const known = new Set(sourcesWithVar.map(s => s.variable));
|
|
2077
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2078
|
+
const m = appendRe.exec(lines[i]);
|
|
2079
|
+
if (!m)
|
|
2080
|
+
continue;
|
|
2081
|
+
const [, obj, args] = m;
|
|
2082
|
+
if (!builders.has(obj) || known.has(obj))
|
|
2083
|
+
continue;
|
|
2084
|
+
// Same-method gate. `sourcesWithVar` is file-wide, so without this a
|
|
2085
|
+
// source named `data` in one method taints a builder appended to in
|
|
2086
|
+
// another method that happens to use the same local name — which is
|
|
2087
|
+
// precisely the Juliet `Bad` / `GoodG2B` pair, and measured as 186
|
|
2088
|
+
// false positives against 82 true ones before the gate.
|
|
2089
|
+
const appendFn = csFnStart(i + 1);
|
|
2090
|
+
const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
|
|
2091
|
+
if (reachable.length > 0 && argsReferenceTaintedVar(args, reachable)) {
|
|
2092
|
+
sourcesWithVar.push({ ...anchor, variable: obj, line: i + 1 });
|
|
2093
|
+
builderSeeds.add(`${obj}@${i + 1}`);
|
|
2094
|
+
added = true;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
1920
2100
|
// A single identifier token; such variables are matched via the fast token
|
|
1921
2101
|
// index below, where membership is exactly equivalent to a word-boundary
|
|
1922
2102
|
// regex — so they need no compiled regex at all.
|
|
@@ -2038,6 +2218,24 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2038
2218
|
if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn)
|
|
2039
2219
|
continue;
|
|
2040
2220
|
}
|
|
2221
|
+
// cognium-dev#530: a builder-seeded source must not escape its method.
|
|
2222
|
+
// The seed stands for "this StringBuilder now holds tainted text",
|
|
2223
|
+
// which is true only of the one object in the one method where the
|
|
2224
|
+
// append happened. The scan above matches by NAME across the whole
|
|
2225
|
+
// file, and Juliet reuses `sourceCode` in `Bad` and in every `Good*`
|
|
2226
|
+
// variant — so without this, a seed created correctly inside `Bad`
|
|
2227
|
+
// linked to the sink inside `GoodG2B1`, whose own `data` is a
|
|
2228
|
+
// hardcoded constant. Measured as 160 false positives.
|
|
2229
|
+
//
|
|
2230
|
+
// Scoped to seeded sources deliberately. Applying a line-range gate
|
|
2231
|
+
// to every C# source would very likely help too, but that changes
|
|
2232
|
+
// matching for all C# consumers and deserves its own measurement.
|
|
2233
|
+
if (csFnRanges.length > 0 && builderSeeds.has(`${source.variable}@${source.line}`)) {
|
|
2234
|
+
const srcFn = csFnStart(source.line);
|
|
2235
|
+
const sinkFn = csFnStart(sink.line);
|
|
2236
|
+
if (srcFn !== sinkFn)
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2041
2239
|
// Simple-identifier sources are already confirmed present by the
|
|
2042
2240
|
// token index; only complex (attribute-path) sources need the
|
|
2043
2241
|
// word-boundary regex to confirm they actually occur in the arg.
|