circle-ir 4.15.0 → 4.16.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/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +134 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +98 -3
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +109 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1176 -119
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +307 -31
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-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 +1349 -58
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +8 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -1
- package/dist/analysis/scalar-derivation.js +172 -0
- package/dist/analysis/scalar-derivation.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +531 -31
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +234 -13
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -3
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +24319 -19934
- package/dist/core/circle-ir-core.cjs +4936 -3575
- package/dist/core/circle-ir-core.js +4936 -3575
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +102 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +102 -43
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* Depends on: sink-filter, constant-propagation
|
|
12
12
|
*/
|
|
13
13
|
import { findJavaSpringReturnXSSSinks } from './language-sources-pass.js';
|
|
14
|
-
import { identifierOutsideSanitizerCalls, propagateTaint } from '../taint-propagation.js';
|
|
14
|
+
import { expressionFullyWrappedBySanitizer, identifierOutsideSanitizerCalls, identifierOutsideSanitizerCallsInText, propagateTaint } from '../taint-propagation.js';
|
|
15
15
|
import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
|
|
16
|
-
import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, findPythonTrustBoundaryViolations, findPythonReturnXSSSinks } from './language-sources-pass.js';
|
|
16
|
+
import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, findPythonTrustBoundaryViolations, findPythonReturnXSSSinks, findJavaWikiPluginReturnXSSSinks } from './language-sources-pass.js';
|
|
17
|
+
import { parseJavaArrayAccessExpression } from '../java-array-element-keys.js';
|
|
17
18
|
import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
|
|
18
19
|
import { walkBackwardDefs } from '../dfg-walk.js';
|
|
19
20
|
import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
21
|
+
import { HTML_CONTEXT_ENCODER_METHODS } from '../constant-propagation/patterns.js';
|
|
20
22
|
import { javaCanonicalStartsWithAllBare } from '../java-canonical-prefix.js';
|
|
21
23
|
/**
|
|
22
24
|
* cognium-dev#328 — the const-prop per-step veto, with two scoped exemptions.
|
|
@@ -42,6 +44,94 @@ import { javaCanonicalStartsWithAllBare } from '../java-canonical-prefix.js';
|
|
|
42
44
|
* code, resolved constant) are unaffected.
|
|
43
45
|
*/
|
|
44
46
|
const CONTAINER_READ_SOURCES = new Set(['plugin_param', 'config_param']);
|
|
47
|
+
/** Method name enclosing `line`, from IR type ranges; null when unknown. */
|
|
48
|
+
function methodAtLine(graph, line) {
|
|
49
|
+
for (const t of graph.ir.types ?? []) {
|
|
50
|
+
for (const m of t.methods) {
|
|
51
|
+
if (line >= m.start_line && line <= m.end_line)
|
|
52
|
+
return m.name;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** #589: method-call sanitizer that is a context-specific HTML / markup encoder. */
|
|
58
|
+
function isHtmlContextEncoderSanitizer(san) {
|
|
59
|
+
if (san.type === 'derived_html_encoder')
|
|
60
|
+
return true; // same-file wrapper of an HTML encoder (#589)
|
|
61
|
+
if (san.type !== 'method_call' || !san.method)
|
|
62
|
+
return false;
|
|
63
|
+
const tail = san.method.replace(/\(\)$/, '').split('.').pop() ?? '';
|
|
64
|
+
return HTML_CONTEXT_ENCODER_METHODS.has(tail);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* #589: true when some def-use chain from `startDefId` back to the flow's
|
|
68
|
+
* source reaches it without passing through a def that a covering sanitizer
|
|
69
|
+
* cleans. A def is covered when a sanitizer for `sinkType` sits on its line
|
|
70
|
+
* (at or after the source line) and none of its predecessor variables
|
|
71
|
+
* appears on that line outside the sanitizer call. Every def on the walk is
|
|
72
|
+
* tested, so a join one or more assignments before the sink is still seen.
|
|
73
|
+
* Unknown defs, chains that reach the source line raw, chains that end in a
|
|
74
|
+
* def with no predecessor, and an exhausted budget all count as uncovered.
|
|
75
|
+
*/
|
|
76
|
+
function hasUncoveredChain(startDefId, sinkType, sourceLine, graph, sanitizersByLine, codeLines) {
|
|
77
|
+
const visited = new Set();
|
|
78
|
+
let budget = 256;
|
|
79
|
+
const covered = (defId, line) => {
|
|
80
|
+
if (line < sourceLine)
|
|
81
|
+
return false;
|
|
82
|
+
const sans = (sanitizersByLine.get(line) ?? []).filter(s => sanitizerCoversSink(s, sinkType));
|
|
83
|
+
if (sans.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
const preds = graph.chainsByToDef.get(defId) ?? [];
|
|
86
|
+
if (preds.length === 0)
|
|
87
|
+
return true;
|
|
88
|
+
const def = graph.defById.get(defId);
|
|
89
|
+
const text = codeLines?.[line - 1];
|
|
90
|
+
let rhs = text;
|
|
91
|
+
if (text && def) {
|
|
92
|
+
const v = def.variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
const m = new RegExp(`(?:^|[^\\w$.])${v}\\s*=(?!=)`).exec(text);
|
|
94
|
+
if (m)
|
|
95
|
+
rhs = text.slice(m.index + m[0].length);
|
|
96
|
+
}
|
|
97
|
+
for (const chain of preds) {
|
|
98
|
+
const predVar = graph.defById.get(chain.from_def)?.variable ?? chain.via;
|
|
99
|
+
const outside = rhs ? identifierOutsideSanitizerCallsInText(predVar, rhs, sans, sinkType) : null;
|
|
100
|
+
// No sanitizer call found in the text: fall back to the single-chain
|
|
101
|
+
// credit (one predecessor that the sanitizer line consumes).
|
|
102
|
+
if (outside === null) {
|
|
103
|
+
if (preds.length > 1)
|
|
104
|
+
return false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (outside)
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
const stack = [startDefId];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
if (--budget < 0)
|
|
115
|
+
return true;
|
|
116
|
+
const id = stack.pop();
|
|
117
|
+
if (visited.has(id))
|
|
118
|
+
continue;
|
|
119
|
+
visited.add(id);
|
|
120
|
+
const def = graph.defById.get(id);
|
|
121
|
+
if (!def)
|
|
122
|
+
return true;
|
|
123
|
+
if (covered(id, def.line))
|
|
124
|
+
continue;
|
|
125
|
+
if (def.line <= sourceLine)
|
|
126
|
+
return true;
|
|
127
|
+
const preds = graph.chainsByToDef.get(id) ?? [];
|
|
128
|
+
if (preds.length === 0)
|
|
129
|
+
return true;
|
|
130
|
+
for (const chain of preds)
|
|
131
|
+
stack.push(chain.from_def);
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
45
135
|
/**
|
|
46
136
|
* #515 — find the real source a Python derived variable actually references.
|
|
47
137
|
* Reads the derivation's right-hand side at its origin line (f-string braces
|
|
@@ -213,7 +303,93 @@ function resolvePythonDerivedAnchor(varName, derived, realSourceByVar, codeLines
|
|
|
213
303
|
}
|
|
214
304
|
return best;
|
|
215
305
|
}
|
|
216
|
-
|
|
306
|
+
/** Drop a trailing `//…` comment, leaving `//` inside string literals alone. */
|
|
307
|
+
function stripJavaTrailingComment(text) {
|
|
308
|
+
let i = 0;
|
|
309
|
+
while (i < text.length) {
|
|
310
|
+
const ch = text[i];
|
|
311
|
+
if (ch === '"') {
|
|
312
|
+
i++;
|
|
313
|
+
while (i < text.length && text[i] !== '"') {
|
|
314
|
+
if (text[i] === '\\') {
|
|
315
|
+
i += 2;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
if (i < text.length)
|
|
321
|
+
i++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (ch === '/' && text[i + 1] === '/')
|
|
325
|
+
return text.slice(0, i);
|
|
326
|
+
i++;
|
|
327
|
+
}
|
|
328
|
+
return text;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* #515 (Java/C#) — like `resolvePythonDerivedAnchor`, pick the real source
|
|
332
|
+
* named on the derivation's right-hand side (following derived chains).
|
|
333
|
+
*/
|
|
334
|
+
function resolveJavaDerivedAnchor(varName, derived, realSourceByVar, codeLines, visiting) {
|
|
335
|
+
if (visiting.has(varName))
|
|
336
|
+
return undefined;
|
|
337
|
+
visiting.add(varName);
|
|
338
|
+
const originLine = derived.get(varName);
|
|
339
|
+
if (originLine === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
const text = codeLines[originLine - 1];
|
|
342
|
+
if (!text)
|
|
343
|
+
return undefined;
|
|
344
|
+
const eq = text.search(/(?<![=!<>+\-*/%&|^:])=(?!=)/);
|
|
345
|
+
if (eq < 0)
|
|
346
|
+
return undefined;
|
|
347
|
+
const rhs = maskStringLiteralsForTokenScan(stripJavaTrailingComment(text.slice(eq + 1)));
|
|
348
|
+
let best;
|
|
349
|
+
for (const m of rhs.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
|
|
350
|
+
const id = m[0];
|
|
351
|
+
if (id === varName)
|
|
352
|
+
continue;
|
|
353
|
+
const real = realSourceByVar.get(id);
|
|
354
|
+
const derivedLine = derived.get(id);
|
|
355
|
+
let cand;
|
|
356
|
+
if (derivedLine !== undefined && (!real || derivedLine > real.line)) {
|
|
357
|
+
cand = resolveJavaDerivedAnchor(id, derived, realSourceByVar, codeLines, visiting);
|
|
358
|
+
}
|
|
359
|
+
if (!cand)
|
|
360
|
+
cand = real;
|
|
361
|
+
if (cand && (!best || cand.line < best.line))
|
|
362
|
+
best = cand;
|
|
363
|
+
}
|
|
364
|
+
return best;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* cognium-dev#624 — expression-scan flows through `lhs = seed.receiverCall()`
|
|
368
|
+
* (e.g. `label = page.getName()`) are vetoed as `variable_not_tainted` because
|
|
369
|
+
* const-prop never seeds those derived names. Argument-style derivations
|
|
370
|
+
* (`shown = encodeForPage(nick)`) must keep the veto so sanitizer credit holds.
|
|
371
|
+
*/
|
|
372
|
+
function javaReceiverChainDerivedVar(code, localVar, seedVar) {
|
|
373
|
+
if (!seedVar || localVar === seedVar)
|
|
374
|
+
return false;
|
|
375
|
+
const esc = seedVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
376
|
+
const receiverOnRhs = new RegExp(`(?:^|[^\\w.$])${esc}\\s*\\.`, 'u');
|
|
377
|
+
const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
378
|
+
const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
379
|
+
for (const line of code.split('\n')) {
|
|
380
|
+
const trimmed = line.trimStart();
|
|
381
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
382
|
+
continue;
|
|
383
|
+
const m = declRe.exec(line) ?? assignRe.exec(line);
|
|
384
|
+
if (!m || m[1] !== localVar)
|
|
385
|
+
continue;
|
|
386
|
+
const rhs = stripJavaTrailingComment(m[2]);
|
|
387
|
+
if (receiverOnRhs.test(rhs))
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers) {
|
|
217
393
|
const lines = typeof code === 'string' ? code.split('\n') : [];
|
|
218
394
|
const selfReassign = new Map();
|
|
219
395
|
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -245,12 +421,40 @@ function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
|
245
421
|
const use = (graph.usesByLine.get(line) ?? []).find(u => u.variable === v && u.def_id != null);
|
|
246
422
|
return use ? graph.defById.get(use.def_id) : undefined;
|
|
247
423
|
};
|
|
248
|
-
return (step, sourceLine, sinkLine) => {
|
|
249
|
-
const
|
|
424
|
+
return (step, sourceLine, sinkLine, sinkType) => {
|
|
425
|
+
const method = methodAtLine(graph, step.line);
|
|
426
|
+
const scopedVar = method ? `${method}:${step.variable}` : step.variable;
|
|
427
|
+
// cognium-dev#751: Go locals are recorded method-scoped only (no unscoped
|
|
428
|
+
// alias) by handleGoAssignment, so Go looks up the scoped name. Other
|
|
429
|
+
// languages keep the unscoped lookup (#614).
|
|
430
|
+
const r = isFalsePositive(constProp, step.line, language === 'go' ? scopedVar : step.variable);
|
|
250
431
|
if (!r.isFalsePositive)
|
|
251
432
|
return false;
|
|
252
433
|
if (r.reason !== 'variable_not_tainted')
|
|
253
434
|
return true;
|
|
435
|
+
// #589: the step variable came from a context-specific encoder
|
|
436
|
+
// (`escapeHtml`, `encodeForHTML`, …), which does not clean `trust_boundary`
|
|
437
|
+
// (a `session.setAttribute` sink). It is "not tainted" only for the other
|
|
438
|
+
// sink types, so this flow is not vetoed.
|
|
439
|
+
// Uncovered entries are scoped (`method:var`). A bare `step.variable` looks
|
|
440
|
+
// up only that step's own method — never any sibling `*:name`.
|
|
441
|
+
if (sinkType !== undefined && constProp.sanitizedVarUncovered) {
|
|
442
|
+
const unc = constProp.sanitizedVarUncovered;
|
|
443
|
+
if (step.variable.includes(':')) {
|
|
444
|
+
if (unc.get(step.variable)?.has(sinkType))
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const method = methodAtLine(graph, step.line);
|
|
449
|
+
if (method) {
|
|
450
|
+
if (unc.get(`${method}:${step.variable}`)?.has(sinkType))
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
else if (unc.get(step.variable)?.has(sinkType)) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
254
458
|
// cognium-dev#579: const-prop has no C# source model, so its tainted set is
|
|
255
459
|
// empty for C# and "not tainted" there means "never looked", not "clean".
|
|
256
460
|
// It still records a plain reassignment (`s = q;`) as a tracked symbol,
|
|
@@ -258,6 +462,43 @@ function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
|
258
462
|
// code and resolved constants (the reasons above) still apply.
|
|
259
463
|
if (language === 'csharp')
|
|
260
464
|
return false;
|
|
465
|
+
// cognium-dev#582: in JS/TS a use after an `if` / `else` / `switch` has the
|
|
466
|
+
// outer def and the block's def both reaching it (the DFG lists one use per
|
|
467
|
+
// reaching def). const-prop has no model of the block's assignment, so its
|
|
468
|
+
// "untainted" is only the outer def's value; when any reaching def is the
|
|
469
|
+
// source's own assignment the verdict is "never looked", not "clean".
|
|
470
|
+
// Keep the veto when that source assignment itself passes through a
|
|
471
|
+
// sanitizer (`n = escapeHtml(req.query.n)` in a branch): skipping would
|
|
472
|
+
// report xss while main and the straight-line form stay clean.
|
|
473
|
+
if ((language === 'javascript' || language === 'typescript')) {
|
|
474
|
+
const reaching = (graph.usesByLine.get(step.line) ?? [])
|
|
475
|
+
.filter(u => u.variable === step.variable && u.def_id != null)
|
|
476
|
+
.map(u => graph.defById.get(u.def_id))
|
|
477
|
+
.filter((d) => !!d);
|
|
478
|
+
const sourceReaches = reaching.filter(d => d.line === sourceLine &&
|
|
479
|
+
sources.some(s => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type)));
|
|
480
|
+
if (reaching.length > 1 && sourceReaches.length > 0) {
|
|
481
|
+
const sansAtSource = (sanitizers ?? []).filter(s => s.line === sourceLine);
|
|
482
|
+
const srcText = lines[sourceLine - 1] ?? '';
|
|
483
|
+
const sourceSanitized = sourceReaches.some(d => {
|
|
484
|
+
const relevant = sinkType
|
|
485
|
+
? sansAtSource.filter(s => sanitizerCoversSink(s, sinkType))
|
|
486
|
+
: sansAtSource;
|
|
487
|
+
if (relevant.length === 0)
|
|
488
|
+
return false;
|
|
489
|
+
const v = escape(d.variable);
|
|
490
|
+
const dm = new RegExp(`(?:^|[^\\w$.])${v}\\s*=(?!=)(.*)$`).exec(srcText);
|
|
491
|
+
if (!dm)
|
|
492
|
+
return false;
|
|
493
|
+
const wrapType = sinkType ?? relevant[0]?.sanitizes?.[0];
|
|
494
|
+
if (!wrapType)
|
|
495
|
+
return false;
|
|
496
|
+
return expressionFullyWrappedBySanitizer(dm[1], relevant, wrapType);
|
|
497
|
+
});
|
|
498
|
+
if (!sourceSanitized)
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
261
502
|
const lo = Math.min(sourceLine, sinkLine);
|
|
262
503
|
// Walk the reaching-def chain back from this step, stepping through
|
|
263
504
|
// self-derived reassignments only. Reaching defs are per method, so a
|
|
@@ -279,8 +520,20 @@ function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
|
279
520
|
// "untainted" can only mean it has no pattern for this source. Container
|
|
280
521
|
// reads are excluded: const-prop tracks collection keys, and there its
|
|
281
522
|
// verdict is informed (`bar = map.get("safeKey")` — 10 OWASP FP files).
|
|
282
|
-
if (d.line === sourceLine && sources.some(s => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type)))
|
|
523
|
+
if (d.line === sourceLine && sources.some(s => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) {
|
|
524
|
+
// cognium-dev#751: sink use after `uid := constantHelper(tainted)` must veto
|
|
525
|
+
// when the helper is in `constantReturningMethods`, even if the flow's
|
|
526
|
+
// source_line is the assignment line (rule E would otherwise keep it).
|
|
527
|
+
if (step.line === sinkLine && !constProp.tainted.has(scopedVar)) {
|
|
528
|
+
for (let ln = sourceLine; ln <= sinkLine; ln++) {
|
|
529
|
+
const calleeMatch = (lines[ln - 1] ?? '').match(/:=\s*(\w+)\s*\(/);
|
|
530
|
+
if (calleeMatch && constProp.constantReturningMethods?.has(calleeMatch[1])) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
283
535
|
return false;
|
|
536
|
+
}
|
|
284
537
|
// (A) the chain crossed at least one `x = f(x)` and ends inside the span.
|
|
285
538
|
if (selfHops > 0 && d.line >= lo)
|
|
286
539
|
return false;
|
|
@@ -296,6 +549,23 @@ export class TaintPropagationPass {
|
|
|
296
549
|
const constProp = ctx.getResult('constant-propagation');
|
|
297
550
|
const sinkFilter = ctx.getResult('sink-filter');
|
|
298
551
|
const { sources, sinks, sanitizers } = sinkFilter;
|
|
552
|
+
// cognium-dev#624: seed receiver-chain derived locals (`label = page.getName()`)
|
|
553
|
+
// into const-prop's tainted set so DFG + expression-scan vetoes keep them.
|
|
554
|
+
// Per-source seeding + receiver filter avoids argument-style derivations
|
|
555
|
+
// (`shown = encodeForPage(nick)`) that must stay vetoed for sanitizer credit.
|
|
556
|
+
if (ctx.language === 'java' && typeof ctx.code === 'string') {
|
|
557
|
+
for (const s of sources) {
|
|
558
|
+
if (!s.variable)
|
|
559
|
+
continue;
|
|
560
|
+
for (const [v] of buildJavaTaintedVars(ctx.code, new Set([s.variable]))) {
|
|
561
|
+
if (v === s.variable)
|
|
562
|
+
continue;
|
|
563
|
+
if (javaReceiverChainDerivedVar(ctx.code, v, s.variable)) {
|
|
564
|
+
constProp.tainted.add(v);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
299
569
|
if (sinks.length === 0) {
|
|
300
570
|
return { flows: [] };
|
|
301
571
|
}
|
|
@@ -307,16 +577,16 @@ export class TaintPropagationPass {
|
|
|
307
577
|
return { flows: [] };
|
|
308
578
|
}
|
|
309
579
|
// DFG-based taint propagation
|
|
310
|
-
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
580
|
+
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers, undefined, ctx.code);
|
|
311
581
|
// cognium-dev#328 — scoped exemption from const-prop's `variable_not_tainted`
|
|
312
582
|
// veto for self-derived reassignment (`x = f(x)`, `x += y`), the #287 shape.
|
|
313
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
583
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language, sanitizers);
|
|
314
584
|
// Filter flows: eliminate dead-code paths and constant-propagation FPs
|
|
315
585
|
const verifiedFlows = propagationResult.flows.filter(flow => {
|
|
316
586
|
if (constProp.unreachableLines.has(flow.sink.line))
|
|
317
587
|
return false;
|
|
318
588
|
for (const step of flow.path) {
|
|
319
|
-
if (isVetoed(step, flow.source.line, flow.sink.line))
|
|
589
|
+
if (isVetoed(step, flow.source.line, flow.sink.line, flow.sink.type))
|
|
320
590
|
return false;
|
|
321
591
|
}
|
|
322
592
|
if (isCorrelatedPredicateFP(constProp, flow))
|
|
@@ -498,6 +768,42 @@ export class TaintPropagationPass {
|
|
|
498
768
|
}
|
|
499
769
|
}
|
|
500
770
|
}
|
|
771
|
+
// Supplement: Java JSPWiki plugin return-XSS flows — cognium-dev#624. The
|
|
772
|
+
// sink synthesised by `LanguageSourcesPass` has no method or argument, so
|
|
773
|
+
// no argument matcher can reach it; link it to its source here, as for the
|
|
774
|
+
// Python return-XSS shape above. The helper already applies its own
|
|
775
|
+
// statement-level sanitizer check, so these flows skip the line-keyed
|
|
776
|
+
// def-chain credit below (a sanitizer elsewhere on a def line of the
|
|
777
|
+
// returned name does not cover it).
|
|
778
|
+
const wikiPluginFlowKeys = new Set();
|
|
779
|
+
if (ctx.language === 'java' && typeof ctx.code === 'string') {
|
|
780
|
+
const xssSanitizers = sanitizers
|
|
781
|
+
.filter(sa => sa.sanitizes.includes('xss'))
|
|
782
|
+
.map(sa => ({ line: sa.line, method: sa.method }));
|
|
783
|
+
for (const rx of findJavaWikiPluginReturnXSSSinks(ctx.code, types, graph.ir.imports, sources, xssSanitizers)) {
|
|
784
|
+
if (constProp.unreachableLines.has(rx.sinkLine))
|
|
785
|
+
continue;
|
|
786
|
+
const sink = sinks.find(sk => sk.line === rx.sinkLine && sk.type === 'xss');
|
|
787
|
+
if (!sink)
|
|
788
|
+
continue;
|
|
789
|
+
const src = sources.find(sc => sc.line === rx.sourceLine);
|
|
790
|
+
if (!src)
|
|
791
|
+
continue;
|
|
792
|
+
wikiPluginFlowKeys.add(`${src.line}:${rx.sinkLine}`);
|
|
793
|
+
pushIfNew({
|
|
794
|
+
source_line: src.line,
|
|
795
|
+
sink_line: rx.sinkLine,
|
|
796
|
+
source_type: src.type,
|
|
797
|
+
sink_type: 'xss',
|
|
798
|
+
path: [
|
|
799
|
+
{ variable: src.variable ?? 'response', line: src.line, type: 'source' },
|
|
800
|
+
{ variable: src.variable ?? 'response', line: rx.sinkLine, type: 'sink' },
|
|
801
|
+
],
|
|
802
|
+
confidence: (src.confidence ?? 1) * (sink.confidence ?? 1),
|
|
803
|
+
sanitized: false,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
501
807
|
// Supplement: array element flows
|
|
502
808
|
// Sprint 82 (#189) — dedup keys on (source_line, sink_line, sink_type) so
|
|
503
809
|
// distinct sink-types at the same call site (e.g. `res.redirect` registered
|
|
@@ -594,19 +900,47 @@ export class TaintPropagationPass {
|
|
|
594
900
|
// explicitly marked sanitized by a guard (e.g. regex-allowlist).
|
|
595
901
|
// Applied to ALL flow generators (DFG-built and the four supplements)
|
|
596
902
|
// so the suppression is uniform regardless of which path emitted the flow.
|
|
903
|
+
//
|
|
904
|
+
// #589: a variable produced by a context-specific encoder (`escapeHtml`,
|
|
905
|
+
// `encodeForHTML`, …) is not clean for `trust_boundary`
|
|
906
|
+
// (`session.setAttribute`), so that flow from the variable stays.
|
|
597
907
|
const sanitizedNames = constProp.sanitizedVars;
|
|
908
|
+
const sanitizedUncovered = constProp.sanitizedVarUncovered;
|
|
598
909
|
let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter(f => {
|
|
599
910
|
if (f.path.length === 0)
|
|
600
911
|
return true;
|
|
601
912
|
const sourceVar = f.path[0].variable;
|
|
602
913
|
if (!sourceVar)
|
|
603
914
|
return true;
|
|
604
|
-
|
|
605
|
-
return
|
|
915
|
+
const coversFlow = (name) => {
|
|
916
|
+
return !(sanitizedUncovered?.get(name)?.has(f.sink_type));
|
|
917
|
+
};
|
|
918
|
+
// #589 review: when any `method:var` sanitizer exists, only that flow's
|
|
919
|
+
// own method can drop it — never a sibling's scoped name (one encoder in
|
|
920
|
+
// doPost must not clear xss in other()), and never the bare alias alone.
|
|
921
|
+
const suffix = `:${sourceVar}`;
|
|
922
|
+
let hasScopedSanitizer = false;
|
|
606
923
|
for (const s of sanitizedNames) {
|
|
607
|
-
if (s.endsWith(
|
|
924
|
+
if (s !== sourceVar && s.endsWith(suffix)) {
|
|
925
|
+
hasScopedSanitizer = true;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (hasScopedSanitizer) {
|
|
930
|
+
if (sourceVar.includes(':') && sanitizedNames.has(sourceVar) && coversFlow(sourceVar))
|
|
608
931
|
return false;
|
|
932
|
+
const method = methodAtLine(graph, f.sink_line) ?? methodAtLine(graph, f.source_line);
|
|
933
|
+
if (method) {
|
|
934
|
+
const scoped = `${method}:${sourceVar}`;
|
|
935
|
+
if (sanitizedNames.has(scoped) && coversFlow(scoped))
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
// Scoped sanitizers exist for this name, but none proved to be this
|
|
939
|
+
// flow's method — keep the flow.
|
|
940
|
+
return true;
|
|
609
941
|
}
|
|
942
|
+
if (sanitizedNames.has(sourceVar) && coversFlow(sourceVar))
|
|
943
|
+
return false;
|
|
610
944
|
return true;
|
|
611
945
|
});
|
|
612
946
|
// Sprint 24 (cognium-dev #102) — uniform line-keyed sanitizer
|
|
@@ -626,6 +960,7 @@ export class TaintPropagationPass {
|
|
|
626
960
|
arr.push(san);
|
|
627
961
|
sanitizersByLine.set(san.line, arr);
|
|
628
962
|
}
|
|
963
|
+
const codeLines = typeof ctx.code === 'string' ? ctx.code.split('\n') : undefined;
|
|
629
964
|
// True unless every def at `line` among `visited` is provably off this
|
|
630
965
|
// flow: not a variable on its path, and a complete backward walk from it
|
|
631
966
|
// never reaches `sourceLine`.
|
|
@@ -670,6 +1005,20 @@ export class TaintPropagationPass {
|
|
|
670
1005
|
}
|
|
671
1006
|
return true;
|
|
672
1007
|
}
|
|
1008
|
+
// #582: Python has no branch-aware def chain, so a literal in a sibling
|
|
1009
|
+
// arm can hide the one def whose sanitizer the walk below would find.
|
|
1010
|
+
// The flow's own source def is the whole answer when its right-hand side
|
|
1011
|
+
// is wrapped, end to end, in a covering sanitizer.
|
|
1012
|
+
if (ctx.language === 'python' && codeLines && f.path.length > 0) {
|
|
1013
|
+
const first = f.path[0];
|
|
1014
|
+
const defText = first.line === f.source_line ? codeLines[f.source_line - 1] : undefined;
|
|
1015
|
+
const sansAtSource = sanitizersByLine.get(f.source_line);
|
|
1016
|
+
if (defText && first.variable && sansAtSource && /^[A-Za-z_]\w*$/.test(first.variable)) {
|
|
1017
|
+
const dm = new RegExp(`^\\s*${first.variable}\\s*=(?!=)(.*)$`).exec(defText);
|
|
1018
|
+
if (dm && expressionFullyWrappedBySanitizer(dm[1], sansAtSource, f.sink_type))
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
673
1022
|
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
674
1023
|
if (sansAtSink && sansAtSink.length > 0) {
|
|
675
1024
|
for (const san of sansAtSink) {
|
|
@@ -698,6 +1047,8 @@ export class TaintPropagationPass {
|
|
|
698
1047
|
// in taint-propagation.ts checkSanitized. Doesn't over-suppress
|
|
699
1048
|
// bare `shlex.quote(host)` on unrelated non-sink lines because
|
|
700
1049
|
// those calls don't create a def in the sink var's chain.
|
|
1050
|
+
if (f.sink_type === 'xss' && wikiPluginFlowKeys.has(`${f.source_line}:${f.sink_line}`))
|
|
1051
|
+
return true;
|
|
701
1052
|
const usesAtSink = graph.usesByLine.get(f.sink_line);
|
|
702
1053
|
if (!usesAtSink || usesAtSink.length === 0)
|
|
703
1054
|
return true;
|
|
@@ -706,12 +1057,29 @@ export class TaintPropagationPass {
|
|
|
706
1057
|
: undefined;
|
|
707
1058
|
if (!sinkVar)
|
|
708
1059
|
return true;
|
|
1060
|
+
// #589: an HTML encoder is credited only when every chain from the
|
|
1061
|
+
// sink back to this flow's source passes through a covering
|
|
1062
|
+
// sanitizer. Computed once per flow, over every reaching def of the
|
|
1063
|
+
// sink variable, and only when an HTML encoder is the candidate.
|
|
1064
|
+
let everyChainCovered;
|
|
1065
|
+
const htmlEncoderCoversEveryChain = () => {
|
|
1066
|
+
if (everyChainCovered === undefined) {
|
|
1067
|
+
const starts = [];
|
|
1068
|
+
for (const u of usesAtSink) {
|
|
1069
|
+
if (u.variable === sinkVar && u.def_id !== null && u.def_id !== undefined)
|
|
1070
|
+
starts.push(u.def_id);
|
|
1071
|
+
}
|
|
1072
|
+
everyChainCovered = starts.length > 0 && starts.every(id => !hasUncoveredChain(id, f.sink_type, f.source_line, graph, sanitizersByLine, codeLines));
|
|
1073
|
+
}
|
|
1074
|
+
return everyChainCovered;
|
|
1075
|
+
};
|
|
709
1076
|
for (const use of usesAtSink) {
|
|
710
1077
|
if (use.variable !== sinkVar)
|
|
711
1078
|
continue;
|
|
712
1079
|
if (use.def_id === null || use.def_id === undefined)
|
|
713
1080
|
continue;
|
|
714
|
-
const
|
|
1081
|
+
const sinkDefId = use.def_id;
|
|
1082
|
+
const walk = walkBackwardDefs(sinkDefId, graph.chainsByToDef, graph.defById, { maxHops: 32 });
|
|
715
1083
|
for (const line of walk.lines) {
|
|
716
1084
|
if (line === f.sink_line)
|
|
717
1085
|
continue; // already checked above
|
|
@@ -737,9 +1105,14 @@ export class TaintPropagationPass {
|
|
|
737
1105
|
if (line > f.source_line && !sanitizedDefOnFlow(walk.visited, line, f))
|
|
738
1106
|
continue;
|
|
739
1107
|
for (const san of sansAtLine) {
|
|
740
|
-
if (sanitizerCoversSink(san, f.sink_type))
|
|
741
|
-
|
|
742
|
-
|
|
1108
|
+
if (!sanitizerCoversSink(san, f.sink_type))
|
|
1109
|
+
continue;
|
|
1110
|
+
// #589: a join anywhere between the sink and the encoder
|
|
1111
|
+
// (`mixed = shown + b`, then `copy = mixed`) must not let an
|
|
1112
|
+
// encoder on one operand drop unrestricted taint from another.
|
|
1113
|
+
if (isHtmlContextEncoderSanitizer(san) && !htmlEncoderCoversEveryChain())
|
|
1114
|
+
continue;
|
|
1115
|
+
return false;
|
|
743
1116
|
}
|
|
744
1117
|
}
|
|
745
1118
|
}
|
|
@@ -894,6 +1267,28 @@ export class TaintPropagationPass {
|
|
|
894
1267
|
});
|
|
895
1268
|
}
|
|
896
1269
|
}
|
|
1270
|
+
// cognium-dev#475: DFG may keep a flow when a local (`v`) was rewritten to
|
|
1271
|
+
// a literal before the sink even though the sink reads a copy (`cmd`) built
|
|
1272
|
+
// earlier. Walk backward from the sink through assignments into the sink
|
|
1273
|
+
// argument; for each upstream local, literal-kill only on lines strictly
|
|
1274
|
+
// before the assignment that copied it into that chain (so `v = "safe"`
|
|
1275
|
+
// after `new SqlCommand(... + v)` does not drop the finding). Unrelated
|
|
1276
|
+
// locals and conditional / re-taint shapes stay sound.
|
|
1277
|
+
if (ctx.language === 'csharp' && typeof ctx.code === 'string') {
|
|
1278
|
+
const code = ctx.code;
|
|
1279
|
+
finalFlows = finalFlows.filter(f => {
|
|
1280
|
+
const deriveLine = csharpSinkDependencyKillVars(code, f);
|
|
1281
|
+
const carriers = csharpFlowTaintCarriers(code, f);
|
|
1282
|
+
for (const [v, beforeLine] of deriveLine) {
|
|
1283
|
+
if (!carriers.has(v))
|
|
1284
|
+
continue;
|
|
1285
|
+
if (csharpLastUnconditionalAssignIsLiteral(code, v, f.source_line, beforeLine, constProp.unreachableLines)) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return true;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
897
1292
|
return { flows: finalFlows };
|
|
898
1293
|
}
|
|
899
1294
|
}
|
|
@@ -1225,8 +1620,118 @@ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLin
|
|
|
1225
1620
|
}
|
|
1226
1621
|
return flows;
|
|
1227
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* #614 — is `varName` tainted in the sink's own method?
|
|
1625
|
+
*
|
|
1626
|
+
* The const-prop result carries scoped names (`method:var`) and, for backward
|
|
1627
|
+
* compatibility, an unscoped alias of each. The unscoped alias says only that
|
|
1628
|
+
* SOME method tainted a local of that name. When any scoped entry for the name
|
|
1629
|
+
* exists, membership is decided by the sink's own scoped name, so a same-named
|
|
1630
|
+
* local in a safe sibling method (the Juliet `bad()` / `good*()` layout) is not
|
|
1631
|
+
* treated as tainted. A name with no scoped entry (a field, a parameter) keeps
|
|
1632
|
+
* the unscoped match. A field use (`this.name` / bare field read) is shared by
|
|
1633
|
+
* every method of its class (SecuriBench Inter4); a local that merely shares a
|
|
1634
|
+
* field's name is not.
|
|
1635
|
+
*/
|
|
1636
|
+
/**
|
|
1637
|
+
* True when `methodName` declares a local (or param) named `varName`. With
|
|
1638
|
+
* overloads, the declaration that encloses `atLine` is the one checked (the
|
|
1639
|
+
* first declaration of that name is only a fallback when no line is known or
|
|
1640
|
+
* none encloses it).
|
|
1641
|
+
*/
|
|
1642
|
+
function methodHasLocalDecl(code, methodName, varName, atLine) {
|
|
1643
|
+
if (!code || !methodName)
|
|
1644
|
+
return false;
|
|
1645
|
+
// Find `methodName(...) [throws ...] {` then scan the body for a local decl.
|
|
1646
|
+
const esc = methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1647
|
+
const v = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1648
|
+
const methodRe = new RegExp('(?:^|[\\s;{}])' + esc + '\\s*\\([^;{]*\\)\\s*(?:throws\\s+[^{]+)?\\{', 'gm');
|
|
1649
|
+
const lineAt = (offset) => {
|
|
1650
|
+
let n = 1;
|
|
1651
|
+
for (let k = 0; k < offset; k++)
|
|
1652
|
+
if (code.charCodeAt(k) === 10)
|
|
1653
|
+
n++;
|
|
1654
|
+
return n;
|
|
1655
|
+
};
|
|
1656
|
+
let chosen = null;
|
|
1657
|
+
let first = null;
|
|
1658
|
+
for (let m = methodRe.exec(code); m; m = methodRe.exec(code)) {
|
|
1659
|
+
const bodyStart = m.index + m[0].length;
|
|
1660
|
+
let i = bodyStart;
|
|
1661
|
+
let depth = 1;
|
|
1662
|
+
while (i < code.length && depth > 0) {
|
|
1663
|
+
const c = code[i++];
|
|
1664
|
+
if (c === '{')
|
|
1665
|
+
depth++;
|
|
1666
|
+
else if (c === '}')
|
|
1667
|
+
depth--;
|
|
1668
|
+
}
|
|
1669
|
+
const cand = { header: m[0], bodyStart, bodyEnd: i - 1 };
|
|
1670
|
+
if (!first)
|
|
1671
|
+
first = cand;
|
|
1672
|
+
if (atLine === undefined)
|
|
1673
|
+
break;
|
|
1674
|
+
if (atLine >= lineAt(m.index) && atLine <= lineAt(cand.bodyEnd)) {
|
|
1675
|
+
chosen = cand;
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const hit = chosen ?? first;
|
|
1680
|
+
if (!hit)
|
|
1681
|
+
return false;
|
|
1682
|
+
const body = code.slice(hit.bodyStart, hit.bodyEnd);
|
|
1683
|
+
// Parameter named varName counts as shadowing the field.
|
|
1684
|
+
if (new RegExp('[(,]\\s*(?:final\\s+)?[\\w.<>,\\[\\]\\s]+\\s+' + v + '\\s*[,)]').test(hit.header))
|
|
1685
|
+
return true;
|
|
1686
|
+
const declRe = new RegExp('(?:^|[;{}])\\s*(?:(?:final|volatile)\\s+)?(?:[A-Za-z_][\\w.<>,\\[\\]\\s]*?)\\s+' + v + '\\s*(?:=|;|,)', 'm');
|
|
1687
|
+
return declRe.test(body);
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* True when `expression` reads this class's own field `varName`: `this.name`,
|
|
1691
|
+
* `self.name`, or `Enclosing.name` (static access through an enclosing type).
|
|
1692
|
+
* Another object's member (`item.name`, `file.name`) is not this class's field.
|
|
1693
|
+
*/
|
|
1694
|
+
function isFieldUseExpression(expression, varName, enclosingTypeNames) {
|
|
1695
|
+
if (!expression)
|
|
1696
|
+
return false;
|
|
1697
|
+
const e = expression.trim();
|
|
1698
|
+
if (e === `this.${varName}` || e === `self.${varName}`)
|
|
1699
|
+
return true;
|
|
1700
|
+
const m = new RegExp('^([A-Za-z_]\\w*)\\.' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$').exec(e);
|
|
1701
|
+
return !!m && enclosingTypeNames.has(m[1]);
|
|
1702
|
+
}
|
|
1703
|
+
/** Names of the types whose range encloses `line` (outer and nested). */
|
|
1704
|
+
function enclosingTypeNamesAt(types, line) {
|
|
1705
|
+
const names = new Set();
|
|
1706
|
+
for (const t of types ?? []) {
|
|
1707
|
+
if (line >= t.start_line && line <= t.end_line)
|
|
1708
|
+
names.add(t.name);
|
|
1709
|
+
}
|
|
1710
|
+
return names;
|
|
1711
|
+
}
|
|
1712
|
+
function isTaintedInMethod(taintedVars, varName, scopedName, fieldNames, isFieldUse) {
|
|
1713
|
+
if (taintedVars.has(scopedName))
|
|
1714
|
+
return true;
|
|
1715
|
+
if (!taintedVars.has(varName))
|
|
1716
|
+
return false;
|
|
1717
|
+
if (scopedName === varName)
|
|
1718
|
+
return true;
|
|
1719
|
+
// Field exception only when THIS use is a field, not when any field has the name.
|
|
1720
|
+
if (isFieldUse && fieldNames.has(varName))
|
|
1721
|
+
return true;
|
|
1722
|
+
const suffix = `:${varName}`;
|
|
1723
|
+
for (const t of taintedVars) {
|
|
1724
|
+
if (t.endsWith(suffix))
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1228
1729
|
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
|
|
1229
1730
|
const flows = [];
|
|
1731
|
+
const fieldNames = new Set();
|
|
1732
|
+
for (const t of types ?? [])
|
|
1733
|
+
for (const f of t.fields ?? [])
|
|
1734
|
+
fieldNames.add(f.name);
|
|
1230
1735
|
const callsByLine = new Map();
|
|
1231
1736
|
for (const call of calls) {
|
|
1232
1737
|
const existing = callsByLine.get(call.location.line) ?? [];
|
|
@@ -1247,7 +1752,16 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
1247
1752
|
if (arg.variable) {
|
|
1248
1753
|
const varName = arg.variable;
|
|
1249
1754
|
const scopedName = call.in_method ? `${call.in_method}:${varName}` : varName;
|
|
1250
|
-
|
|
1755
|
+
// Bare `name` that is a field read (no local of that name in this
|
|
1756
|
+
// method) still counts as a field use — Inter4-style. A shadowing
|
|
1757
|
+
// local declaration in the method does not.
|
|
1758
|
+
const expr = arg.expression ?? '';
|
|
1759
|
+
const explicitField = isFieldUseExpression(expr, varName, enclosingTypeNamesAt(types, call.location.line));
|
|
1760
|
+
const bareFieldRead = fieldNames.has(varName)
|
|
1761
|
+
&& expr === varName
|
|
1762
|
+
&& !methodHasLocalDecl(code, call.in_method, varName, call.location.line);
|
|
1763
|
+
const isFieldUse = explicitField || bareFieldRead;
|
|
1764
|
+
if (isTaintedInMethod(taintedVars, varName, scopedName, fieldNames, isFieldUse)) {
|
|
1251
1765
|
// Sprint 13 #70 — pick a source in the same method scope as the
|
|
1252
1766
|
// sink, not blanket sources[0]. The varName-based match is
|
|
1253
1767
|
// primary; method-scope is the tiebreaker; sources[0] is the
|
|
@@ -1365,23 +1879,22 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
|
|
|
1365
1879
|
!sink.argPositions.includes(arg.position)) {
|
|
1366
1880
|
continue;
|
|
1367
1881
|
}
|
|
1368
|
-
const
|
|
1369
|
-
if (
|
|
1370
|
-
const arrayName =
|
|
1371
|
-
const indexStr = arrayAccessMatch[2];
|
|
1882
|
+
const arrayAccess = parseJavaArrayAccessExpression(arg.expression);
|
|
1883
|
+
if (arrayAccess) {
|
|
1884
|
+
const { base, arrayName, indexKey, displayVar } = arrayAccess;
|
|
1372
1885
|
const taintedIndices = taintedArrayElements.get(arrayName);
|
|
1373
1886
|
if (taintedIndices) {
|
|
1374
|
-
const isTainted = taintedIndices.has(
|
|
1887
|
+
const isTainted = taintedIndices.has(indexKey) || taintedIndices.has('*');
|
|
1375
1888
|
if (isTainted) {
|
|
1376
1889
|
// Sprint 13 #70 — method-scoped source picker (see detectCollectionFlows).
|
|
1377
|
-
const source = pickScopedSource(sources, sink.line, call.in_method ?? null, types,
|
|
1890
|
+
const source = pickScopedSource(sources, sink.line, call.in_method ?? null, types, base);
|
|
1378
1891
|
if (source) {
|
|
1379
1892
|
flows.push({
|
|
1380
1893
|
source_line: source.line, sink_line: sink.line,
|
|
1381
1894
|
source_type: source.type, sink_type: sink.type,
|
|
1382
1895
|
path: [
|
|
1383
|
-
{ variable:
|
|
1384
|
-
{ variable:
|
|
1896
|
+
{ variable: base, line: source.line, type: 'source' },
|
|
1897
|
+
{ variable: displayVar, line: sink.line, type: 'sink' },
|
|
1385
1898
|
],
|
|
1386
1899
|
confidence: 0.85, sanitized: false,
|
|
1387
1900
|
});
|
|
@@ -1394,6 +1907,20 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
|
|
|
1394
1907
|
}
|
|
1395
1908
|
return flows;
|
|
1396
1909
|
}
|
|
1910
|
+
/** Parameter names referenced as identifiers inside a compound sink argument (#685). */
|
|
1911
|
+
function compoundArgParamNames(expression, paramNames) {
|
|
1912
|
+
const stripped = expression.replace(/(['"`])(?:\\.|(?!\1).)*\1/g, (m) => ' '.repeat(m.length));
|
|
1913
|
+
const matched = [];
|
|
1914
|
+
for (const raw of paramNames) {
|
|
1915
|
+
const name = raw.startsWith('*') ? raw.slice(1) : raw;
|
|
1916
|
+
if (!/^[\w$]+$/.test(name))
|
|
1917
|
+
continue;
|
|
1918
|
+
const re = new RegExp(`(?<![\\w$.])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$])`);
|
|
1919
|
+
if (re.test(stripped))
|
|
1920
|
+
matched.push(name);
|
|
1921
|
+
}
|
|
1922
|
+
return matched;
|
|
1923
|
+
}
|
|
1397
1924
|
function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines, tainted, code) {
|
|
1398
1925
|
const flows = [];
|
|
1399
1926
|
const paramSourcesByMethod = new Map();
|
|
@@ -1432,14 +1959,17 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
1432
1959
|
if (!methodParamSources)
|
|
1433
1960
|
continue;
|
|
1434
1961
|
for (const arg of call.arguments) {
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1962
|
+
if (sink.argPositions && sink.argPositions.length > 0 &&
|
|
1963
|
+
!sink.argPositions.includes(arg.position)) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const varNames = arg.variable
|
|
1967
|
+
? [arg.variable]
|
|
1968
|
+
: (arg.expression
|
|
1969
|
+
? compoundArgParamNames(arg.expression, methodParamSources.keys())
|
|
1970
|
+
: []);
|
|
1971
|
+
for (const varName of varNames) {
|
|
1972
|
+
const paramSource = methodParamSources.get(varName);
|
|
1443
1973
|
if (paramSource) {
|
|
1444
1974
|
// Sprint 82 (#189) — sink-type-aware dedup so a sink registered
|
|
1445
1975
|
// under multiple types at the same call site (e.g. `redirect` as
|
|
@@ -1447,7 +1977,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
1447
1977
|
const exists = flows.some(f => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
1448
1978
|
if (!exists) {
|
|
1449
1979
|
if (typeof code === 'string' &&
|
|
1450
|
-
isReassignedToLiteralBetween(code,
|
|
1980
|
+
isReassignedToLiteralBetween(code, varName, paramSource.line, sink.line, unreachableLines)) {
|
|
1451
1981
|
continue;
|
|
1452
1982
|
}
|
|
1453
1983
|
// Note: DFG-flow filter handles sanitizer-guard suppression
|
|
@@ -1456,8 +1986,8 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
1456
1986
|
source_line: paramSource.line, sink_line: sink.line,
|
|
1457
1987
|
source_type: paramSource.type, sink_type: sink.type,
|
|
1458
1988
|
path: [
|
|
1459
|
-
{ variable:
|
|
1460
|
-
{ variable:
|
|
1989
|
+
{ variable: varName, line: paramSource.line, type: 'source' },
|
|
1990
|
+
{ variable: varName, line: sink.line, type: 'sink' },
|
|
1461
1991
|
],
|
|
1462
1992
|
confidence: 0.75, sanitized: false,
|
|
1463
1993
|
});
|
|
@@ -1623,7 +2153,18 @@ function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
|
1623
2153
|
}
|
|
1624
2154
|
continue;
|
|
1625
2155
|
}
|
|
1626
|
-
|
|
2156
|
+
let d = declRe.exec(text) ?? assignRe.exec(text);
|
|
2157
|
+
if (!d && text.includes('{')) {
|
|
2158
|
+
const inner = text.slice(text.indexOf('{') + 1, text.lastIndexOf('}'));
|
|
2159
|
+
for (const stmt of inner.split(';')) {
|
|
2160
|
+
const trimmedStmt = stmt.trim();
|
|
2161
|
+
if (!trimmedStmt)
|
|
2162
|
+
continue;
|
|
2163
|
+
d = declRe.exec(`${trimmedStmt};`) ?? assignRe.exec(`${trimmedStmt};`);
|
|
2164
|
+
if (d)
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1627
2168
|
if (!d)
|
|
1628
2169
|
continue;
|
|
1629
2170
|
const [, lhs, rhs] = d;
|
|
@@ -1658,6 +2199,211 @@ function splitLinesCached(code) {
|
|
|
1658
2199
|
_reassignSplitLines = code.split('\n');
|
|
1659
2200
|
return _reassignSplitLines;
|
|
1660
2201
|
}
|
|
2202
|
+
const CSHARP_ID_KW = new Set([
|
|
2203
|
+
'if', 'else', 'for', 'foreach', 'while', 'do', 'switch', 'case', 'default', 'break',
|
|
2204
|
+
'return', 'new', 'var', 'string', 'int', 'bool', 'void', 'static', 'public', 'class',
|
|
2205
|
+
'using', 'lock', 'try', 'catch', 'finally', 'throw', 'null', 'true', 'false',
|
|
2206
|
+
]);
|
|
2207
|
+
function csharpStripStringLiterals(text) {
|
|
2208
|
+
return text
|
|
2209
|
+
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
2210
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
2211
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ')
|
|
2212
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, ' ');
|
|
2213
|
+
}
|
|
2214
|
+
function csharpIdentifiersInExpr(text) {
|
|
2215
|
+
const ids = csharpStripStringLiterals(text).match(/\b[A-Za-z_][\w]*\b/g) ?? [];
|
|
2216
|
+
return ids.filter(id => !CSHARP_ID_KW.has(id));
|
|
2217
|
+
}
|
|
2218
|
+
const CSHARP_ASSIGN_RE = /^\s*(?:(?:var|string|String|[\w.<>\[\]?]+\s+)?([A-Za-z_]\w*)|([A-Za-z_]\w*))\s*=\s*(.+)$/;
|
|
2219
|
+
/**
|
|
2220
|
+
* Variables that carry THIS C# flow's taint (path locals plus forward/backward
|
|
2221
|
+
* derivation). Pure-literal helpers (`table = "users"` next to tainted `input`)
|
|
2222
|
+
* are excluded so their literal assignments cannot drop the flow (#475).
|
|
2223
|
+
*/
|
|
2224
|
+
function csharpFlowTaintCarriers(code, flow) {
|
|
2225
|
+
const carriers = new Set();
|
|
2226
|
+
for (const step of flow.path) {
|
|
2227
|
+
if (step.variable)
|
|
2228
|
+
carriers.add(step.variable);
|
|
2229
|
+
}
|
|
2230
|
+
if (carriers.size === 0)
|
|
2231
|
+
return carriers;
|
|
2232
|
+
const lines = splitLinesCached(code);
|
|
2233
|
+
const lo = Math.max(0, flow.source_line - 1);
|
|
2234
|
+
const hi = Math.min(lines.length, flow.sink_line);
|
|
2235
|
+
const stripLits = (rhs) => csharpStripStringLiterals(rhs);
|
|
2236
|
+
const assigns = [];
|
|
2237
|
+
for (let i = lo; i < hi; i++) {
|
|
2238
|
+
const line = lines[i] ?? '';
|
|
2239
|
+
if (/^\s*if\b/.test(line))
|
|
2240
|
+
continue;
|
|
2241
|
+
for (const seg of line.split(';')) {
|
|
2242
|
+
const t = seg.trim();
|
|
2243
|
+
if (!t)
|
|
2244
|
+
continue;
|
|
2245
|
+
const m = CSHARP_ASSIGN_RE.exec(t);
|
|
2246
|
+
if (!m)
|
|
2247
|
+
continue;
|
|
2248
|
+
const lhs = m[1] ?? m[2];
|
|
2249
|
+
const rhs = m[3];
|
|
2250
|
+
if (!lhs)
|
|
2251
|
+
continue;
|
|
2252
|
+
const rhsCode = stripLits(rhs);
|
|
2253
|
+
const literal = rhsCode.replace(/\s+/g, '') === '';
|
|
2254
|
+
const rhsIdents = rhsCode.match(/\b[A-Za-z_][\w]*\b/g) ?? [];
|
|
2255
|
+
assigns.push({ lhs, rhsIdents, literal });
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const hadNonLiteral = new Set();
|
|
2259
|
+
for (const a of assigns) {
|
|
2260
|
+
if (!a.literal)
|
|
2261
|
+
hadNonLiteral.add(a.lhs);
|
|
2262
|
+
}
|
|
2263
|
+
let changed = true;
|
|
2264
|
+
for (let guard = 0; changed && guard < assigns.length + 2; guard++) {
|
|
2265
|
+
changed = false;
|
|
2266
|
+
for (const a of assigns) {
|
|
2267
|
+
if (!carriers.has(a.lhs) && a.rhsIdents.some(id => carriers.has(id))) {
|
|
2268
|
+
carriers.add(a.lhs);
|
|
2269
|
+
changed = true;
|
|
2270
|
+
}
|
|
2271
|
+
if (carriers.has(a.lhs)) {
|
|
2272
|
+
for (const id of a.rhsIdents) {
|
|
2273
|
+
if (!carriers.has(id) && hadNonLiteral.has(id)) {
|
|
2274
|
+
carriers.add(id);
|
|
2275
|
+
changed = true;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return carriers;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* For each upstream local, the 1-based line of the assignment that copied it
|
|
2285
|
+
* into the sink dependency chain (closest to the sink). A literal reset of
|
|
2286
|
+
* `v` after `var q = ... + v` must not kill a flow that only reads `q` (#475).
|
|
2287
|
+
*/
|
|
2288
|
+
function csharpSinkDependencyKillVars(code, flow) {
|
|
2289
|
+
const lines = splitLinesCached(code);
|
|
2290
|
+
const sinkIdx = flow.sink_line - 1;
|
|
2291
|
+
const deriveLine = new Map();
|
|
2292
|
+
if (sinkIdx < 0 || sinkIdx >= lines.length)
|
|
2293
|
+
return deriveLine;
|
|
2294
|
+
const need = new Set(csharpIdentifiersInExpr(lines[sinkIdx] ?? ''));
|
|
2295
|
+
for (let i = sinkIdx - 1; i >= flow.source_line - 1; i--) {
|
|
2296
|
+
const line = lines[i] ?? '';
|
|
2297
|
+
for (const seg of line.split(';')) {
|
|
2298
|
+
const t = seg.trim();
|
|
2299
|
+
if (!t)
|
|
2300
|
+
continue;
|
|
2301
|
+
const m = CSHARP_ASSIGN_RE.exec(t);
|
|
2302
|
+
if (!m)
|
|
2303
|
+
continue;
|
|
2304
|
+
const lhs = m[1] ?? m[2];
|
|
2305
|
+
if (!lhs || !need.has(lhs))
|
|
2306
|
+
continue;
|
|
2307
|
+
for (const id of csharpIdentifiersInExpr(m[3])) {
|
|
2308
|
+
if (!deriveLine.has(id))
|
|
2309
|
+
deriveLine.set(id, i + 1);
|
|
2310
|
+
need.add(id);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return deriveLine;
|
|
2315
|
+
}
|
|
2316
|
+
function csharpLastUnconditionalAssignIsLiteral(code, variable, srcLine,
|
|
2317
|
+
/** Scan assignments on lines strictly before this 1-based line. */
|
|
2318
|
+
beforeLine, unreachableLines) {
|
|
2319
|
+
if (!variable || !/^[A-Za-z_][\w]*$/.test(variable))
|
|
2320
|
+
return false;
|
|
2321
|
+
if (beforeLine - srcLine < 1)
|
|
2322
|
+
return false;
|
|
2323
|
+
const lines = splitLinesCached(code);
|
|
2324
|
+
const reLit = new RegExp('^(?:(?:var|string|String)\\s+)?' + variable + '\\s*(?<![+\\-*/%&|^])=\\s*(?:"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|`(?:[^`\\\\]|\\\\.)*`)\\s*$');
|
|
2325
|
+
const rePlain = new RegExp('^(?:(?:var|string|String)\\s+)?' + variable + '\\s*(?<![+\\-*/%&|^])=\\s*(.+)\\s*$');
|
|
2326
|
+
const reCompound = new RegExp('^(?:(?:var|string|String)\\s+)?' + variable + '\\s*[+\\-*/%&|^]=\\s*(.+)\\s*$');
|
|
2327
|
+
const reRefOut = new RegExp('\\b(?:ref|out)\\s+' + variable + '\\b');
|
|
2328
|
+
let last = null;
|
|
2329
|
+
const lo = Math.max(0, srcLine);
|
|
2330
|
+
const hi = Math.min(lines.length, beforeLine - 1);
|
|
2331
|
+
for (let i = lo; i < hi; i++) {
|
|
2332
|
+
if (unreachableLines?.has(i + 1))
|
|
2333
|
+
continue;
|
|
2334
|
+
const line = lines[i];
|
|
2335
|
+
if (!line)
|
|
2336
|
+
continue;
|
|
2337
|
+
if (/^\s*if\b/.test(line))
|
|
2338
|
+
continue;
|
|
2339
|
+
for (const seg of line.split(';')) {
|
|
2340
|
+
const t = seg.trim();
|
|
2341
|
+
if (!t)
|
|
2342
|
+
continue;
|
|
2343
|
+
if (reCompound.test(t)) {
|
|
2344
|
+
last = 'other';
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (reRefOut.test(csharpStripStringLiterals(t))) {
|
|
2348
|
+
last = 'other';
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (!rePlain.test(t))
|
|
2352
|
+
continue;
|
|
2353
|
+
if (csharpAssignInConditionalBlock(lines, i))
|
|
2354
|
+
continue;
|
|
2355
|
+
if (reLit.test(t)) {
|
|
2356
|
+
last = 'literal';
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
last = 'other';
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
return last === 'literal';
|
|
2364
|
+
}
|
|
2365
|
+
/** True when `header` opens a non-bare control-flow block (not a method/type body). */
|
|
2366
|
+
function csharpBlockHeaderIsConditional(header) {
|
|
2367
|
+
const h = header.trim();
|
|
2368
|
+
if (!h || h === '{')
|
|
2369
|
+
return false;
|
|
2370
|
+
if (/^(?:case\b.*|default\s*):$/.test(h))
|
|
2371
|
+
return true;
|
|
2372
|
+
if (/\bswitch\b/.test(h))
|
|
2373
|
+
return true;
|
|
2374
|
+
if (/^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(h))
|
|
2375
|
+
return true;
|
|
2376
|
+
if (/^(?:\}\s*)?else\b/.test(h))
|
|
2377
|
+
return true;
|
|
2378
|
+
if (/\b(?:for|foreach|while|do|using|lock|try|catch|finally)\b/.test(h))
|
|
2379
|
+
return true;
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* True when line `idx` sits in a control-flow block. Walks outward through
|
|
2384
|
+
* enclosing braces so an Allman `for` / `while` header above a lone `{` line
|
|
2385
|
+
* is recognised (#475).
|
|
2386
|
+
*/
|
|
2387
|
+
function csharpAssignInConditionalBlock(lines, idx) {
|
|
2388
|
+
let at = idx;
|
|
2389
|
+
for (let guard = 0; guard < 16; guard++) {
|
|
2390
|
+
const block = enclosingBrace(lines, at, 0);
|
|
2391
|
+
if (!block)
|
|
2392
|
+
return false;
|
|
2393
|
+
let header = block.header.trim();
|
|
2394
|
+
if (!header || header === '{') {
|
|
2395
|
+
const prev = block.headerLine > 0 ? (lines[block.headerLine - 1] ?? '').trim() : '';
|
|
2396
|
+
if (prev)
|
|
2397
|
+
header = prev;
|
|
2398
|
+
}
|
|
2399
|
+
if (csharpBlockHeaderIsConditional(header))
|
|
2400
|
+
return true;
|
|
2401
|
+
if (block.headerLine <= 0)
|
|
2402
|
+
return false;
|
|
2403
|
+
at = block.headerLine - 1;
|
|
2404
|
+
}
|
|
2405
|
+
return false;
|
|
2406
|
+
}
|
|
1661
2407
|
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
1662
2408
|
if (!variable || sinkLine - srcLine < 2)
|
|
1663
2409
|
return false;
|
|
@@ -1688,17 +2434,33 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreach
|
|
|
1688
2434
|
// The fall-through `break;` is optional. Used by cognium-dev #101
|
|
1689
2435
|
// to suppress the switch→constant FP corpus.
|
|
1690
2436
|
const reSwitchCase = new RegExp(`^\\s*(?:case\\b.*?|default\\s*):\\s*${variable}\\s*=\\s*${strLit}\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
2437
|
+
const lineHasNakedLiteralReassign = (line) => {
|
|
2438
|
+
if (reNaked.test(line) || reSwitchCase.test(line))
|
|
2439
|
+
return true;
|
|
2440
|
+
// cognium-dev#475: `var v = input; v = "const";` on one line — only the
|
|
2441
|
+
// trailing statement is a literal kill, not the whole physical line.
|
|
2442
|
+
for (const seg of line.split(';')) {
|
|
2443
|
+
const t = seg.trim();
|
|
2444
|
+
if (!t)
|
|
2445
|
+
continue;
|
|
2446
|
+
if (reNaked.test(t) || reSwitchCase.test(t))
|
|
2447
|
+
return true;
|
|
2448
|
+
}
|
|
2449
|
+
return false;
|
|
2450
|
+
};
|
|
1691
2451
|
for (let i = lo; i < hi; i++) {
|
|
1692
2452
|
const line = lines[i];
|
|
1693
2453
|
if (!line)
|
|
1694
2454
|
continue;
|
|
1695
2455
|
if (reGuarded.test(line))
|
|
1696
2456
|
return true;
|
|
1697
|
-
if (!
|
|
2457
|
+
if (!lineHasNakedLiteralReassign(line))
|
|
1698
2458
|
continue;
|
|
1699
2459
|
// A literal assigned in one case of a switch only overwrites the value
|
|
1700
2460
|
// when no case gives the variable anything else. `case 'A': bar = param;`
|
|
1701
2461
|
// next to `default: bar = "safe";` leaves `bar` tainted on the 'A' path.
|
|
2462
|
+
if (javaCatchLiteralHasTaintedTryArm(lines, i, variable, strLit, unreachableLines))
|
|
2463
|
+
continue;
|
|
1702
2464
|
if (siblingBranchAssignsNonLiteral(lines, i, variable, strLit, unreachableLines))
|
|
1703
2465
|
continue;
|
|
1704
2466
|
return true;
|
|
@@ -1745,6 +2507,46 @@ function matchingClose(lines, line, col) {
|
|
|
1745
2507
|
}
|
|
1746
2508
|
return null;
|
|
1747
2509
|
}
|
|
2510
|
+
/**
|
|
2511
|
+
* True when line `idx` is a literal assignment in a `catch` block and the
|
|
2512
|
+
* paired `try` assigns `variable` a non-literal value (cognium-dev#753).
|
|
2513
|
+
*/
|
|
2514
|
+
function javaCatchLiteralHasTaintedTryArm(lines, idx, variable, strLit, unreachableLines) {
|
|
2515
|
+
const block = enclosingBrace(lines, idx, 0);
|
|
2516
|
+
if (!block || !/\bcatch\b/.test(block.header))
|
|
2517
|
+
return false;
|
|
2518
|
+
let h = block.headerLine;
|
|
2519
|
+
while (h >= 0 && !/\btry\b/.test(lines[h]))
|
|
2520
|
+
h--;
|
|
2521
|
+
if (h < 0)
|
|
2522
|
+
return false;
|
|
2523
|
+
const tryIdx = lines[h].search(/\btry\b/);
|
|
2524
|
+
let openLine = h;
|
|
2525
|
+
let openCol = lines[h].indexOf('{', tryIdx);
|
|
2526
|
+
if (openCol < 0) {
|
|
2527
|
+
openLine = h + 1;
|
|
2528
|
+
while (openLine < lines.length && lines[openLine].trim() === '')
|
|
2529
|
+
openLine++;
|
|
2530
|
+
if (openLine >= lines.length)
|
|
2531
|
+
return false;
|
|
2532
|
+
openCol = lines[openLine].indexOf('{');
|
|
2533
|
+
if (openCol < 0)
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
const tryEnd = matchingClose(lines, openLine, openCol);
|
|
2537
|
+
if (!tryEnd)
|
|
2538
|
+
return false;
|
|
2539
|
+
const assignTry = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*$`);
|
|
2540
|
+
const literalOnly = new RegExp(`^${strLit}$`);
|
|
2541
|
+
for (let i = openLine; i <= tryEnd.line; i++) {
|
|
2542
|
+
if (unreachableLines?.has(i + 1))
|
|
2543
|
+
continue;
|
|
2544
|
+
const m = assignTry.exec(lines[i]);
|
|
2545
|
+
if (m && m[1] && !literalOnly.test(m[1]))
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
return false;
|
|
2549
|
+
}
|
|
1748
2550
|
/**
|
|
1749
2551
|
* True when line `idx` assigns `variable` in one arm of a `switch` or one
|
|
1750
2552
|
* branch of an `if`/`else` chain, and a sibling arm or branch assigns it a
|
|
@@ -1767,7 +2569,7 @@ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachabl
|
|
|
1767
2569
|
block = enclosingBrace(lines, block.line, block.col);
|
|
1768
2570
|
}
|
|
1769
2571
|
if (!block)
|
|
1770
|
-
return
|
|
2572
|
+
return pythonSiblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines);
|
|
1771
2573
|
let first = block.headerLine;
|
|
1772
2574
|
let last;
|
|
1773
2575
|
const end = matchingClose(lines, block.line, block.col);
|
|
@@ -1827,6 +2629,9 @@ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachabl
|
|
|
1827
2629
|
last = next.line;
|
|
1828
2630
|
}
|
|
1829
2631
|
}
|
|
2632
|
+
else if (/\bcatch\b/.test(block.header)) {
|
|
2633
|
+
return javaCatchLiteralHasTaintedTryArm(lines, idx, variable, strLit, unreachableLines);
|
|
2634
|
+
}
|
|
1830
2635
|
else {
|
|
1831
2636
|
return false;
|
|
1832
2637
|
}
|
|
@@ -1850,6 +2655,83 @@ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachabl
|
|
|
1850
2655
|
}
|
|
1851
2656
|
return false;
|
|
1852
2657
|
}
|
|
2658
|
+
const PY_ARM_OPEN = /^(?:(?:if|elif)\b.*|else\s*):\s*(?:#.*)?$/;
|
|
2659
|
+
/**
|
|
2660
|
+
* The indentation-delimited counterpart of `siblingBranchAssignsNonLiteral`.
|
|
2661
|
+
* The arms of one `if` / `elif` / `else` chain are the header lines at one
|
|
2662
|
+
* indentation plus the more deeply indented lines under each. True when line
|
|
2663
|
+
* `idx` sits directly in one arm of such a chain and another arm assigns
|
|
2664
|
+
* `variable` a value that is not a string literal (cognium-dev#582). Answers
|
|
2665
|
+
* false, keeping the literal as an overwrite, for anything else: a
|
|
2666
|
+
* straight-line assignment, a `for` / `while` / `try` / `def` body, or a header
|
|
2667
|
+
* that is not part of an `if` chain.
|
|
2668
|
+
*/
|
|
2669
|
+
function pythonSiblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
2670
|
+
const indentOf = (t) => t.length - t.trimStart().length;
|
|
2671
|
+
const skippable = (t) => t.trim() === '' || t.trimStart().startsWith('#');
|
|
2672
|
+
const own = lines[idx];
|
|
2673
|
+
if (own === undefined)
|
|
2674
|
+
return false;
|
|
2675
|
+
const bodyIndent = indentOf(own);
|
|
2676
|
+
if (bodyIndent === 0)
|
|
2677
|
+
return false;
|
|
2678
|
+
// The header of the arm that holds the literal: the nearest line above with less indentation.
|
|
2679
|
+
let h = idx - 1;
|
|
2680
|
+
while (h >= 0 && (skippable(lines[h]) || indentOf(lines[h]) >= bodyIndent))
|
|
2681
|
+
h--;
|
|
2682
|
+
if (h < 0 || !PY_ARM_OPEN.test(lines[h].trim()))
|
|
2683
|
+
return false;
|
|
2684
|
+
const armIndent = indentOf(lines[h]);
|
|
2685
|
+
// The last line of the arm whose header is at `header`.
|
|
2686
|
+
const armEnd = (header) => {
|
|
2687
|
+
let lastBody = header;
|
|
2688
|
+
for (let e = header + 1; e < lines.length; e++) {
|
|
2689
|
+
if (skippable(lines[e]))
|
|
2690
|
+
continue;
|
|
2691
|
+
if (indentOf(lines[e]) <= armIndent)
|
|
2692
|
+
break;
|
|
2693
|
+
lastBody = e;
|
|
2694
|
+
}
|
|
2695
|
+
return lastBody;
|
|
2696
|
+
};
|
|
2697
|
+
// Walk up through `elif` / `else` to the opening `if`.
|
|
2698
|
+
let first = h;
|
|
2699
|
+
while (/^(?:elif\b|else\s*:)/.test(lines[first].trim())) {
|
|
2700
|
+
let p = first - 1;
|
|
2701
|
+
while (p >= 0 && (skippable(lines[p]) || indentOf(lines[p]) > armIndent))
|
|
2702
|
+
p--;
|
|
2703
|
+
if (p < 0 || indentOf(lines[p]) !== armIndent || !PY_ARM_OPEN.test(lines[p].trim()))
|
|
2704
|
+
return false;
|
|
2705
|
+
first = p;
|
|
2706
|
+
}
|
|
2707
|
+
if (!/^if\b/.test(lines[first].trim()))
|
|
2708
|
+
return false;
|
|
2709
|
+
// Walk down through the following `elif` / `else` arms.
|
|
2710
|
+
const ownEnd = armEnd(h);
|
|
2711
|
+
let last = ownEnd;
|
|
2712
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
2713
|
+
let n = last + 1;
|
|
2714
|
+
while (n < lines.length && skippable(lines[n]))
|
|
2715
|
+
n++;
|
|
2716
|
+
if (n >= lines.length || indentOf(lines[n]) !== armIndent)
|
|
2717
|
+
break;
|
|
2718
|
+
if (!/^(?:elif\b|else\s*:)/.test(lines[n].trim()) || !PY_ARM_OPEN.test(lines[n].trim()))
|
|
2719
|
+
break;
|
|
2720
|
+
last = armEnd(n);
|
|
2721
|
+
}
|
|
2722
|
+
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*$`);
|
|
2723
|
+
const literalOnly = new RegExp(`^${strLit}$`);
|
|
2724
|
+
for (let i = first; i <= last; i++) {
|
|
2725
|
+
if (i === idx || unreachableLines?.has(i + 1))
|
|
2726
|
+
continue;
|
|
2727
|
+
if (i >= h && i <= ownEnd)
|
|
2728
|
+
continue; // the literal's own arm is not a sibling
|
|
2729
|
+
const m = assign.exec(lines[i]);
|
|
2730
|
+
if (m && m[1] && !literalOnly.test(m[1]))
|
|
2731
|
+
return true;
|
|
2732
|
+
}
|
|
2733
|
+
return false;
|
|
2734
|
+
}
|
|
1853
2735
|
/**
|
|
1854
2736
|
* Languages whose plain quoted strings cannot interpolate, so blanking the
|
|
1855
2737
|
* literal text before identifier tokenisation is sound (cognium-dev#353).
|
|
@@ -2057,6 +2939,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2057
2939
|
// is every Juliet C# file.
|
|
2058
2940
|
const csFnRanges = methodRanges(['csharp']);
|
|
2059
2941
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
2942
|
+
const jsFnRanges = methodRanges(['javascript', 'typescript']);
|
|
2943
|
+
const jsFnStart = (line) => enclosingStart(jsFnRanges, line);
|
|
2060
2944
|
// cognium-dev#548 — names declared as class fields. A field is the one kind
|
|
2061
2945
|
// of variable whose value legitimately crosses from one method to another, so
|
|
2062
2946
|
// the per-method gate below must not apply to it.
|
|
@@ -2500,6 +3384,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2500
3384
|
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
2501
3385
|
const fileAnchor = sourcesWithVar.reduce((a, sv) => (sv.line < a.line ? sv : a), sourcesWithVar[0]);
|
|
2502
3386
|
const have = new Set(sourcesWithVar.map(sv => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
3387
|
+
const seedVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
3388
|
+
const derivedForAnchor = buildJavaTaintedVars(code, seedVars);
|
|
3389
|
+
const codeLinesForAnchor = code.split('\n');
|
|
2503
3390
|
for (const alias of aliases) {
|
|
2504
3391
|
const fn = methodStartOf(alias.line);
|
|
2505
3392
|
const isField = csFieldNames.has(alias.variable);
|
|
@@ -2512,14 +3399,34 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2512
3399
|
// call), which also keeps it ahead of a sink in a method declared
|
|
2513
3400
|
// earlier in the file than the method holding the source.
|
|
2514
3401
|
const inMethod = sourcesWithVar.filter(sv => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
2515
|
-
|
|
3402
|
+
let from = !alias.carried && inMethod.length > 0
|
|
2516
3403
|
? inMethod.reduce((a, sv) => (sv.line < a.line ? sv : a), inMethod[0])
|
|
2517
3404
|
: fileAnchor;
|
|
2518
|
-
|
|
3405
|
+
// cognium-dev#515: same RHS anchor as Java — do not credit an earlier decoy read.
|
|
3406
|
+
if (!alias.carried && derivedForAnchor.size > 0) {
|
|
3407
|
+
const methodRealSourceByVar = new Map();
|
|
3408
|
+
for (const s of sourcesWithVar) {
|
|
3409
|
+
if (methodStartOf(s.line) !== fn)
|
|
3410
|
+
continue;
|
|
3411
|
+
const prev = methodRealSourceByVar.get(s.variable);
|
|
3412
|
+
if (!prev || s.line < prev.line)
|
|
3413
|
+
methodRealSourceByVar.set(s.variable, s);
|
|
3414
|
+
}
|
|
3415
|
+
const referenced = resolveJavaDerivedAnchor(alias.variable, derivedForAnchor, methodRealSourceByVar, codeLinesForAnchor, new Set());
|
|
3416
|
+
if (referenced)
|
|
3417
|
+
from = referenced;
|
|
3418
|
+
}
|
|
3419
|
+
const aliasEntry = {
|
|
2519
3420
|
...from,
|
|
2520
3421
|
variable: alias.variable,
|
|
2521
3422
|
...(alias.carried || inMethod.length === 0 ? { line: alias.line } : {}),
|
|
2522
|
-
}
|
|
3423
|
+
};
|
|
3424
|
+
// cognium-dev#327: field aliases must not keep the writing method's
|
|
3425
|
+
// `in_method` tag — readers in other methods need to see the taint.
|
|
3426
|
+
if (isField) {
|
|
3427
|
+
delete aliasEntry.in_method;
|
|
3428
|
+
}
|
|
3429
|
+
sourcesWithVar.push(aliasEntry);
|
|
2523
3430
|
}
|
|
2524
3431
|
}
|
|
2525
3432
|
else if ((language === 'java' || language === 'csharp') && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
@@ -2550,29 +3457,73 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2550
3457
|
}
|
|
2551
3458
|
return null;
|
|
2552
3459
|
};
|
|
3460
|
+
const typeAt = (line) => {
|
|
3461
|
+
for (const t of types ?? []) {
|
|
3462
|
+
if (line >= t.start_line && line <= t.end_line)
|
|
3463
|
+
return t;
|
|
3464
|
+
}
|
|
3465
|
+
return undefined;
|
|
3466
|
+
};
|
|
3467
|
+
const isDeclaredField = (line, name) => {
|
|
3468
|
+
const t = typeAt(line);
|
|
3469
|
+
return t?.fields?.some(f => f.name === name) ?? false;
|
|
3470
|
+
};
|
|
2553
3471
|
const existingVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
3472
|
+
const codeLines = code.split('\n');
|
|
3473
|
+
const realSourceByVar = new Map();
|
|
3474
|
+
for (const s of sourcesWithVar) {
|
|
3475
|
+
const prev = realSourceByVar.get(s.variable);
|
|
3476
|
+
if (!prev || s.line < prev.line)
|
|
3477
|
+
realSourceByVar.set(s.variable, s);
|
|
3478
|
+
}
|
|
2554
3479
|
for (const [varName, derivedLine] of derived) {
|
|
2555
3480
|
if (!varName || existingVars.has(varName))
|
|
2556
3481
|
continue;
|
|
2557
3482
|
const owner = methodAt(derivedLine);
|
|
3483
|
+
const fieldAlias = isDeclaredField(derivedLine, varName);
|
|
2558
3484
|
let scopedAnchor = anchor;
|
|
2559
3485
|
if (owner) {
|
|
2560
3486
|
let best;
|
|
3487
|
+
const methodRealSourceByVar = new Map();
|
|
2561
3488
|
for (const s of sourcesWithVar) {
|
|
2562
3489
|
const sOwner = s.in_method ?? methodAt(s.line);
|
|
2563
3490
|
if (sOwner !== owner)
|
|
2564
3491
|
continue;
|
|
3492
|
+
const prev = methodRealSourceByVar.get(s.variable);
|
|
3493
|
+
if (!prev || s.line < prev.line)
|
|
3494
|
+
methodRealSourceByVar.set(s.variable, s);
|
|
2565
3495
|
if (!best || s.line < best.line)
|
|
2566
3496
|
best = s;
|
|
2567
3497
|
}
|
|
2568
|
-
|
|
3498
|
+
const referenced = resolveJavaDerivedAnchor(varName, derived, methodRealSourceByVar, codeLines, new Set());
|
|
3499
|
+
if (referenced)
|
|
3500
|
+
scopedAnchor = referenced;
|
|
3501
|
+
else if (best)
|
|
2569
3502
|
scopedAnchor = best;
|
|
2570
3503
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3504
|
+
// Keep scopedAnchor.line for in_method / preceding-source matching
|
|
3505
|
+
// (#614). Pass derivedLine only for literal-kill (#753), so a safe
|
|
3506
|
+
// sibling method's earlier sink cannot pick up this alias by line.
|
|
3507
|
+
// cognium-dev#327: a declared field written from a method parameter
|
|
3508
|
+
// must not inherit the writing method as `in_method` — same exemption
|
|
3509
|
+
// as request sources on fields (4.12.0), so readers in other methods
|
|
3510
|
+
// can see the taint.
|
|
3511
|
+
if (fieldAlias) {
|
|
3512
|
+
const { in_method: _drop, ...rest } = scopedAnchor;
|
|
3513
|
+
sourcesWithVar.push({
|
|
3514
|
+
...rest,
|
|
3515
|
+
variable: varName,
|
|
3516
|
+
literal_kill_line: derivedLine,
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
else {
|
|
3520
|
+
sourcesWithVar.push({
|
|
3521
|
+
...scopedAnchor,
|
|
3522
|
+
variable: varName,
|
|
3523
|
+
literal_kill_line: derivedLine,
|
|
3524
|
+
...(owner ? { in_method: owner } : {}),
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
2576
3527
|
existingVars.add(varName);
|
|
2577
3528
|
}
|
|
2578
3529
|
}
|
|
@@ -2586,7 +3537,217 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2586
3537
|
// tainted var (at the CommandText line) so the arg-scan links the Execute sink.
|
|
2587
3538
|
if (language === 'csharp' && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
2588
3539
|
const lines = code.split('\n');
|
|
2589
|
-
|
|
3540
|
+
// cognium-dev#594: `X.StartInfo.FileName = <tainted>` is the same shape for a
|
|
3541
|
+
// `System.Diagnostics.Process`: the executable path rides the process object
|
|
3542
|
+
// and the zero-argument `X.Start()` is the injection point. Only the
|
|
3543
|
+
// `FileName` write seeds it, and an `Arguments` write when the program is a
|
|
3544
|
+
// shell (`ArgumentList` is a separate follow-up), so a constant `FileName`
|
|
3545
|
+
// beside an unrelated tainted parameter stays clean.
|
|
3546
|
+
// A write only: `+=` or a single `=` that is not part of `==`, `!=`, `<=`, `>=`
|
|
3547
|
+
// (`if (p.StartInfo.FileName == tool)` is a comparison, not an assignment).
|
|
3548
|
+
// `StartInfo.Arguments` seeds too, but only behind a shell (or a non-constant
|
|
3549
|
+
// program), see `argumentsReachShell` below.
|
|
3550
|
+
const ctRe = /(\w+)\s*\.\s*(?:CommandText|StartInfo\s*\.\s*(?:FileName|(Arguments)))\s*(?:\+=|(?<![=!<>])=(?!=))\s*(.+)/;
|
|
3551
|
+
// cognium-dev#594: the same two writes on a local `ProcessStartInfo psi`
|
|
3552
|
+
// (`psi.FileName = …`, `psi.Arguments = …`), read back by `Process.Start(psi)`.
|
|
3553
|
+
// Gated on the receiver being declared as a `ProcessStartInfo`, so a `FileName`
|
|
3554
|
+
// or `Arguments` property of any other type seeds nothing.
|
|
3555
|
+
const psiVars = new Set();
|
|
3556
|
+
for (const line of lines) {
|
|
3557
|
+
for (const pm of line.matchAll(/\bProcessStartInfo\s+([\p{L}_][\p{L}\p{N}_]*)/gu))
|
|
3558
|
+
psiVars.add(pm[1]);
|
|
3559
|
+
for (const pm of line.matchAll(/\b([\p{L}_][\p{L}\p{N}_]*)\s*=\s*new\s+ProcessStartInfo\b/gu))
|
|
3560
|
+
psiVars.add(pm[1]);
|
|
3561
|
+
}
|
|
3562
|
+
const psiRe = /(?<![\w.])(\w+)\s*\.\s*(?:FileName|(Arguments))\s*(?:\+=|(?<![=!<>])=(?!=))\s*(.+)/;
|
|
3563
|
+
const psiOverwrittenByLiteral = (obj, prop, at) => {
|
|
3564
|
+
const fn = csFnStart(at + 1);
|
|
3565
|
+
const re = new RegExp(`(?<![\\w.])${obj}\\s*\\.\\s*${prop}\\s*=(?!=)\\s*@?"[^"]*"\\s*;\\s*(?://.*)?$`);
|
|
3566
|
+
for (let j = at + 1; j < lines.length; j++) {
|
|
3567
|
+
if (csFnStart(j + 1) !== fn)
|
|
3568
|
+
break;
|
|
3569
|
+
if (re.test(lines[j]))
|
|
3570
|
+
return true;
|
|
3571
|
+
}
|
|
3572
|
+
return false;
|
|
3573
|
+
};
|
|
3574
|
+
const fileNameRe = (obj) => new RegExp(`\\b${obj}\\s*\\.\\s*(?:StartInfo\\s*\\.\\s*)?FileName\\s*=(?!=)\\s*(.+)`);
|
|
3575
|
+
const shellProgramRe = /^(?:cmd|powershell|pwsh|sh|bash|zsh|dash|ksh|csh|tcsh|fish)(?:\.exe)?$/i;
|
|
3576
|
+
// cognium-dev#594: with a constant, non-shell program the argument string is
|
|
3577
|
+
// argv (the #272 / #328 safe shape), so a tainted `Arguments` does not by itself
|
|
3578
|
+
// reach a shell. A constant shell program, or a program that is not a constant,
|
|
3579
|
+
// lets the argument text choose what runs. No `FileName` write in the method
|
|
3580
|
+
// (for example a `ProcessStartInfo` built elsewhere) seeds nothing.
|
|
3581
|
+
const argumentsReachShell = (obj, line) => {
|
|
3582
|
+
const re = fileNameRe(obj);
|
|
3583
|
+
const fnStart = csFnStart(line);
|
|
3584
|
+
for (let j = 0; j < lines.length; j++) {
|
|
3585
|
+
if (csFnStart(j + 1) !== fnStart)
|
|
3586
|
+
continue;
|
|
3587
|
+
const fm = re.exec(lines[j]);
|
|
3588
|
+
if (!fm)
|
|
3589
|
+
continue;
|
|
3590
|
+
const lit = /^\s*@?"([^"]*)"\s*;?\s*(?:\/\/.*)?$/.exec(fm[1]);
|
|
3591
|
+
if (!lit)
|
|
3592
|
+
return true;
|
|
3593
|
+
const base = lit[1].split(/[\\/]/).pop() ?? '';
|
|
3594
|
+
return shellProgramRe.test(base);
|
|
3595
|
+
}
|
|
3596
|
+
return false;
|
|
3597
|
+
};
|
|
3598
|
+
// cognium-dev#594: `X.StartInfo.ArgumentList.Add(<tainted>)` (or `psi.ArgumentList.Add(…)`
|
|
3599
|
+
// on a local `ProcessStartInfo`) is argv, so it only reaches a command when the
|
|
3600
|
+
// program is a shell that is told to run a command string. Seed only when the
|
|
3601
|
+
// method writes `FileName` exactly once, to a constant shell program, and an
|
|
3602
|
+
// `ArgumentList.Add` of a command flag (`-c`, `/c`, …) is in the same method.
|
|
3603
|
+
// A non-constant or non-shell program, a second `FileName` write, an
|
|
3604
|
+
// `ArgumentList.Clear()`, or an `Add(` that wraps over several lines seeds nothing.
|
|
3605
|
+
const argListRe = /(?<![\w.])(\w+)\s*\.\s*(StartInfo\s*\.\s*)?ArgumentList\s*\.\s*Add\s*\(\s*(.+?)\s*\)\s*;?\s*(?:\/\/.*)?$/;
|
|
3606
|
+
const shellFlagRe = /^(?:[-/](?:c|k|command|encodedcommand|ec))$/i;
|
|
3607
|
+
// cognium-dev#750: the program may also be written by the `ProcessStartInfo`
|
|
3608
|
+
// constructor's single argument or by a `FileName = …` entry of its object
|
|
3609
|
+
// initializer, on the statement that creates `obj` (`var psi = new
|
|
3610
|
+
// ProcessStartInfo("/bin/sh");`, `new ProcessStartInfo { FileName = "/bin/sh",
|
|
3611
|
+
// … }`, possibly wrapped over several lines). Each counts as one `FileName`
|
|
3612
|
+
// write, so the exactly-one-write rule still holds, and they apply only when the
|
|
3613
|
+
// method has no explicit `obj.FileName = …` write. Any shape that is not
|
|
3614
|
+
// plainly one constant program (two constructor arguments, an `Arguments`
|
|
3615
|
+
// entry, a non-literal value) counts as a non-constant write.
|
|
3616
|
+
const splitTopLevel = (text) => {
|
|
3617
|
+
const parts = [];
|
|
3618
|
+
let depth = 0;
|
|
3619
|
+
let quote = '';
|
|
3620
|
+
let cur = '';
|
|
3621
|
+
for (let k = 0; k < text.length; k++) {
|
|
3622
|
+
const ch = text[k];
|
|
3623
|
+
if (quote) {
|
|
3624
|
+
cur += ch;
|
|
3625
|
+
if (ch === '\\' && quote === '"') {
|
|
3626
|
+
cur += text[++k] ?? '';
|
|
3627
|
+
continue;
|
|
3628
|
+
}
|
|
3629
|
+
if (ch === quote)
|
|
3630
|
+
quote = '';
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
if (ch === '"') {
|
|
3634
|
+
quote = ch;
|
|
3635
|
+
cur += ch;
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3638
|
+
if (ch === '(' || ch === '{' || ch === '[')
|
|
3639
|
+
depth++;
|
|
3640
|
+
if (ch === ')' || ch === '}' || ch === ']')
|
|
3641
|
+
depth--;
|
|
3642
|
+
if (ch === ',' && depth === 0) {
|
|
3643
|
+
parts.push(cur.trim());
|
|
3644
|
+
cur = '';
|
|
3645
|
+
continue;
|
|
3646
|
+
}
|
|
3647
|
+
cur += ch;
|
|
3648
|
+
}
|
|
3649
|
+
if (cur.trim())
|
|
3650
|
+
parts.push(cur.trim());
|
|
3651
|
+
return parts;
|
|
3652
|
+
};
|
|
3653
|
+
const constLiteral = (expr) => {
|
|
3654
|
+
const m = /^@?"([^"]*)"$/.exec(expr.trim());
|
|
3655
|
+
return m ? (m[1].split(/[\\/]/).pop() ?? '') : null;
|
|
3656
|
+
};
|
|
3657
|
+
// Returns null when line j does not create `obj` or the creation writes no program.
|
|
3658
|
+
const startInfoCreation = (obj, j) => {
|
|
3659
|
+
const head = new RegExp(`(?<![\\w.])${obj}(?:\\s*\\.\\s*StartInfo)?\\s*=(?!=)\\s*new\\s+ProcessStartInfo\\b`).exec(lines[j]);
|
|
3660
|
+
if (!head)
|
|
3661
|
+
return null;
|
|
3662
|
+
let text = '';
|
|
3663
|
+
for (let k = j; k < lines.length && k < j + 40; k++) {
|
|
3664
|
+
text += (k === j ? lines[k].slice(head.index) : ' ' + lines[k]).replace(/\/\/.*$/, '');
|
|
3665
|
+
if (text.includes(';'))
|
|
3666
|
+
break;
|
|
3667
|
+
}
|
|
3668
|
+
const m = /new\s+ProcessStartInfo\s*(?:\(([^()]*)\))?\s*(?:\{([\s\S]*)\})?\s*;/.exec(text);
|
|
3669
|
+
if (!m)
|
|
3670
|
+
return { program: null };
|
|
3671
|
+
let program;
|
|
3672
|
+
const ctorArgs = splitTopLevel(m[1] ?? '');
|
|
3673
|
+
if (ctorArgs.length > 1)
|
|
3674
|
+
return { program: null };
|
|
3675
|
+
if (ctorArgs.length === 1)
|
|
3676
|
+
program = constLiteral(ctorArgs[0]);
|
|
3677
|
+
for (const member of splitTopLevel(m[2] ?? '')) {
|
|
3678
|
+
const em = /^(\w+)\s*=(?!=)\s*([\s\S]*)$/.exec(member);
|
|
3679
|
+
if (!em)
|
|
3680
|
+
return { program: null };
|
|
3681
|
+
if (em[1] === 'FileName') {
|
|
3682
|
+
if (program !== undefined)
|
|
3683
|
+
return { program: null };
|
|
3684
|
+
program = constLiteral(em[2]);
|
|
3685
|
+
}
|
|
3686
|
+
else if (em[1] === 'Arguments' || em[1] === 'ArgumentList') {
|
|
3687
|
+
return { program: null };
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
return program === undefined ? null : { program };
|
|
3691
|
+
};
|
|
3692
|
+
const constStringFieldProgram = (fieldName) => {
|
|
3693
|
+
const declRe = new RegExp(`\\b(?:\\w+\\s+)*const\\s+string\\s+${fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*@?"([^"]*)"\\s*;`);
|
|
3694
|
+
let hits = 0;
|
|
3695
|
+
let base = null;
|
|
3696
|
+
for (const srcLine of lines) {
|
|
3697
|
+
const dm = declRe.exec(srcLine);
|
|
3698
|
+
if (!dm)
|
|
3699
|
+
continue;
|
|
3700
|
+
hits++;
|
|
3701
|
+
base = dm[1].split(/[\\/]/).pop() ?? '';
|
|
3702
|
+
}
|
|
3703
|
+
return hits === 1 ? base : null;
|
|
3704
|
+
};
|
|
3705
|
+
const argListReachesShell = (obj, line) => {
|
|
3706
|
+
const fnStart = csFnStart(line);
|
|
3707
|
+
const nameRe = fileNameRe(obj);
|
|
3708
|
+
const clearRe = new RegExp(`(?<![\\w.])${obj}\\s*\\.\\s*(?:StartInfo\\s*\\.\\s*)?ArgumentList\\s*\\.\\s*(?:Clear|Remove\\w*|Insert\\w*)\\s*\\(`);
|
|
3709
|
+
const addRe = new RegExp(`(?<![\\w.])${obj}\\s*\\.\\s*(?:StartInfo\\s*\\.\\s*)?ArgumentList\\s*\\.\\s*Add\\s*\\(\\s*@?"([^"]*)"\\s*\\)`);
|
|
3710
|
+
let program = null;
|
|
3711
|
+
let writes = 0;
|
|
3712
|
+
let creations = 0;
|
|
3713
|
+
let createdProgram = null;
|
|
3714
|
+
let flag = false;
|
|
3715
|
+
for (let j = 0; j < lines.length; j++) {
|
|
3716
|
+
if (csFnStart(j + 1) !== fnStart)
|
|
3717
|
+
continue;
|
|
3718
|
+
if (clearRe.test(lines[j]))
|
|
3719
|
+
return false;
|
|
3720
|
+
const fm = nameRe.exec(lines[j]);
|
|
3721
|
+
if (fm) {
|
|
3722
|
+
writes++;
|
|
3723
|
+
const lit = /^\s*@?"([^"]*)"\s*;?\s*(?:\/\/.*)?$/.exec(fm[1]);
|
|
3724
|
+
if (lit) {
|
|
3725
|
+
program = lit[1].split(/[\\/]/).pop() ?? '';
|
|
3726
|
+
}
|
|
3727
|
+
else {
|
|
3728
|
+
const id = /^\s*(\w+)\s*;?\s*(?:\/\/.*)?$/.exec(fm[1]);
|
|
3729
|
+
program = id ? constStringFieldProgram(id[1]) : null;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
const created = startInfoCreation(obj, j);
|
|
3733
|
+
if (created) {
|
|
3734
|
+
creations++;
|
|
3735
|
+
createdProgram = created.program;
|
|
3736
|
+
}
|
|
3737
|
+
const am = addRe.exec(lines[j]);
|
|
3738
|
+
if (am && shellFlagRe.test(am[1]))
|
|
3739
|
+
flag = true;
|
|
3740
|
+
}
|
|
3741
|
+
// An explicit `obj.FileName = …` decides the program, as before. Only when the
|
|
3742
|
+
// method has none does the creating statement's constant program count, and
|
|
3743
|
+
// then it must be the only creation (a second `new ProcessStartInfo(…)` for
|
|
3744
|
+
// the same variable could replace it).
|
|
3745
|
+
if (writes === 0 && creations > 0) {
|
|
3746
|
+
writes = creations;
|
|
3747
|
+
program = createdProgram;
|
|
3748
|
+
}
|
|
3749
|
+
return writes === 1 && program !== null && shellProgramRe.test(program) && flag;
|
|
3750
|
+
};
|
|
2590
3751
|
// cognium-dev#548: seed per method, from sources in that method. The seed
|
|
2591
3752
|
// used to be made once per file and matched by name, so `cmd.CommandText =
|
|
2592
3753
|
// … + data` in `Bad()` tainted `cmd.ExecuteScalar()` in `GoodG2B()`, whose
|
|
@@ -2599,15 +3760,60 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2599
3760
|
added = false;
|
|
2600
3761
|
guard++;
|
|
2601
3762
|
for (let i = 0; i < lines.length; i++) {
|
|
2602
|
-
const
|
|
3763
|
+
const am = argListRe.exec(lines[i]);
|
|
3764
|
+
if (am) {
|
|
3765
|
+
const [, aobj, viaStartInfo, aargs] = am;
|
|
3766
|
+
const akey = `${aobj}@${csFnStart(i + 1)}`;
|
|
3767
|
+
if ((viaStartInfo || psiVars.has(aobj)) && !seeded.has(akey) && argListReachesShell(aobj, i + 1)) {
|
|
3768
|
+
// A value set to a literal after the source and before this `Add` no longer
|
|
3769
|
+
// carries it (`cmd = name; cmd = "uptime"; …Add(cmd)`).
|
|
3770
|
+
const literalSince = (v, from) => {
|
|
3771
|
+
const re = new RegExp(`(?<![\\w.])${v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=(?!=)\\s*@?"[^"]*"\\s*;`);
|
|
3772
|
+
const fn = csFnStart(i + 1);
|
|
3773
|
+
for (let j = from; j < i; j++)
|
|
3774
|
+
if (csFnStart(j + 1) === fn && re.test(lines[j]))
|
|
3775
|
+
return true;
|
|
3776
|
+
return false;
|
|
3777
|
+
};
|
|
3778
|
+
const reachableA = sourcesWithVar.filter(sv => sv.line <= i + 1 && !csDifferentMethod(sv.variable, sv.line, i + 1) && !literalSince(sv.variable, sv.line));
|
|
3779
|
+
const maskedArg = aargs.replace(/\b(?:int|uint|long|ulong|short|ushort|byte|double|float|decimal|bool|Int16|Int32|Int64|Double|Decimal|Boolean|Guid)\s*\.\s*(?:Try)?Parse\s*\((?:[^()]|\([^()]*\))*\)|\bConvert\s*\.\s*To(?:Int16|Int32|Int64|Double|Decimal|Boolean|Byte)\s*\((?:[^()]|\([^()]*\))*\)/g, '0');
|
|
3780
|
+
if (reachableA.length > 0 && argsReferenceTaintedVar(maskedArg, reachableA)) {
|
|
3781
|
+
const fromA = reachableA.reduce((a, sv) => (sv.line < a.line ? sv : a), reachableA[0]);
|
|
3782
|
+
sourcesWithVar.push({ ...fromA, variable: aobj, line: i + 1 });
|
|
3783
|
+
seeded.add(akey);
|
|
3784
|
+
added = true;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
let m = ctRe.exec(lines[i]);
|
|
3790
|
+
let viaLocal = false;
|
|
3791
|
+
if (!m) {
|
|
3792
|
+
const pm = psiRe.exec(lines[i]);
|
|
3793
|
+
if (pm && psiVars.has(pm[1])) {
|
|
3794
|
+
m = pm;
|
|
3795
|
+
viaLocal = true;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
2603
3798
|
if (!m)
|
|
2604
3799
|
continue;
|
|
2605
|
-
const [, obj, rhs] = m;
|
|
3800
|
+
const [, obj, isArguments, rhs] = m;
|
|
3801
|
+
// A later plain-literal write of the same property in this method replaces
|
|
3802
|
+
// the tainted value before `Process.Start(psi)` reads it: do not credit it.
|
|
3803
|
+
if (viaLocal && psiOverwrittenByLiteral(obj, isArguments ? 'Arguments' : 'FileName', i))
|
|
3804
|
+
continue;
|
|
2606
3805
|
const key = `${obj}@${csFnStart(i + 1)}`;
|
|
2607
3806
|
if (seeded.has(key))
|
|
2608
3807
|
continue;
|
|
3808
|
+
if (isArguments && !argumentsReachShell(obj, i + 1))
|
|
3809
|
+
continue;
|
|
2609
3810
|
const reachable = sourcesWithVar.filter(sv => sv.line <= i + 1 && !csDifferentMethod(sv.variable, sv.line, i + 1));
|
|
2610
|
-
|
|
3811
|
+
// A value converted to a number / Guid / bool cannot carry a command; the
|
|
3812
|
+
// local-`psi` form masks such conversions so a raw operand beside one still counts.
|
|
3813
|
+
const rhsForTaint = viaLocal
|
|
3814
|
+
? rhs.replace(/\b(?:int|uint|long|ulong|short|ushort|byte|double|float|decimal|bool|Int16|Int32|Int64|Double|Decimal|Boolean|Guid)\s*\.\s*(?:Try)?Parse\s*\((?:[^()]|\([^()]*\))*\)|\bConvert\s*\.\s*To(?:Int16|Int32|Int64|Double|Decimal|Boolean|Byte)\s*\((?:[^()]|\([^()]*\))*\)/g, '0')
|
|
3815
|
+
: rhs;
|
|
3816
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhsForTaint, reachable))
|
|
2611
3817
|
continue;
|
|
2612
3818
|
const from = reachable.reduce((a, sv) => (sv.line < a.line ? sv : a), reachable[0]);
|
|
2613
3819
|
sourcesWithVar.push({ ...from, variable: obj, line: i + 1 });
|
|
@@ -2807,6 +4013,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2807
4013
|
if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn)
|
|
2808
4014
|
continue;
|
|
2809
4015
|
}
|
|
4016
|
+
// cognium-dev#752: GraphQL resolver args are seeded by name only; scope
|
|
4017
|
+
// them to the resolver (or factory) they came from so a same-named
|
|
4018
|
+
// local in another recorded function does not inherit the seed.
|
|
4019
|
+
if (jsFnRanges.length > 0 &&
|
|
4020
|
+
typeof source.location === 'string' &&
|
|
4021
|
+
source.location.startsWith('GraphQL ')) {
|
|
4022
|
+
const sinkFn = jsFnStart(sink.line);
|
|
4023
|
+
if (sinkFn !== -1) {
|
|
4024
|
+
const range = jsFnRanges.find(([start]) => start === sinkFn);
|
|
4025
|
+
if (range && (source.line < range[0] || source.line > range[1]))
|
|
4026
|
+
continue;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
2810
4029
|
// cognium-dev#530: a builder-seeded source must not escape its method.
|
|
2811
4030
|
// The seed stands for "this StringBuilder now holds tainted text",
|
|
2812
4031
|
// which is true only of the one object in the one method where the
|
|
@@ -2860,14 +4079,16 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2860
4079
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
2861
4080
|
break;
|
|
2862
4081
|
}
|
|
2863
|
-
// Sprint 9 #58.3 / #56: between
|
|
2864
|
-
// the tainted variable is reassigned to a
|
|
2865
|
-
// (either naked `var = "lit"` or guarded by an
|
|
2866
|
-
// such as `if (!ALLOWLIST.contains(var)) var = "lit"` /
|
|
4082
|
+
// Sprint 9 #58.3 / #56: between the alias derivation (or source)
|
|
4083
|
+
// line and sink.line, if the tainted variable is reassigned to a
|
|
4084
|
+
// pure string literal (either naked `var = "lit"` or guarded by an
|
|
4085
|
+
// allowlist check such as `if (!ALLOWLIST.contains(var)) var = "lit"` /
|
|
2867
4086
|
// `if var not in ALLOWLIST: var = "lit"`), the original taint
|
|
2868
|
-
// no longer reaches the sink — suppress the flow.
|
|
4087
|
+
// no longer reaches the sink — suppress the flow. Derived aliases
|
|
4088
|
+
// carry `literal_kill_line` so kill starts at the assignment that
|
|
4089
|
+
// created them (#753), while `source.line` stays on the anchor (#614).
|
|
2869
4090
|
if (typeof code === 'string' &&
|
|
2870
|
-
isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
4091
|
+
isReassignedToLiteralBetween(code, source.variable, source.literal_kill_line ?? source.line, sink.line, unreachableLines)) {
|
|
2871
4092
|
break;
|
|
2872
4093
|
}
|
|
2873
4094
|
// Note: DFG-flow filter handles sanitizer-guard suppression
|
|
@@ -2890,6 +4111,73 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2890
4111
|
}
|
|
2891
4112
|
}
|
|
2892
4113
|
}
|
|
4114
|
+
// cognium-dev#490 — same helper as `splitCSharpDepth0Statements` in language-sources-pass.
|
|
4115
|
+
const splitCSharpDepth0Statements = (line) => {
|
|
4116
|
+
if (line.includes('{'))
|
|
4117
|
+
return [line];
|
|
4118
|
+
const statements = [];
|
|
4119
|
+
let start = 0;
|
|
4120
|
+
let depth = 0;
|
|
4121
|
+
let inStr = null;
|
|
4122
|
+
let esc = false;
|
|
4123
|
+
for (let i = 0; i < line.length; i++) {
|
|
4124
|
+
const c = line[i];
|
|
4125
|
+
if (inStr) {
|
|
4126
|
+
if (esc) {
|
|
4127
|
+
esc = false;
|
|
4128
|
+
continue;
|
|
4129
|
+
}
|
|
4130
|
+
if (c === '\\') {
|
|
4131
|
+
esc = true;
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
if (c === inStr)
|
|
4135
|
+
inStr = null;
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4138
|
+
if (c === '"' || c === "'") {
|
|
4139
|
+
inStr = c;
|
|
4140
|
+
continue;
|
|
4141
|
+
}
|
|
4142
|
+
if (c === '(' || c === '[')
|
|
4143
|
+
depth++;
|
|
4144
|
+
else if (c === ')' || c === ']')
|
|
4145
|
+
depth = Math.max(0, depth - 1);
|
|
4146
|
+
else if (c === ';' && depth === 0) {
|
|
4147
|
+
const part = line.slice(start, i + 1).trim();
|
|
4148
|
+
if (part)
|
|
4149
|
+
statements.push(part);
|
|
4150
|
+
start = i + 1;
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
const tail = line.slice(start).trim();
|
|
4154
|
+
if (tail)
|
|
4155
|
+
statements.push(tail);
|
|
4156
|
+
return statements.length > 1 ? statements : [line];
|
|
4157
|
+
};
|
|
4158
|
+
const boundInEarlierStatement = (code, variable, sinkMethod) => {
|
|
4159
|
+
if (!variable || variable === '<inline>' || !sinkMethod)
|
|
4160
|
+
return false;
|
|
4161
|
+
const stmts = splitCSharpDepth0Statements(code);
|
|
4162
|
+
if (stmts.length < 2)
|
|
4163
|
+
return false;
|
|
4164
|
+
const methodRe = new RegExp(`\\b${sinkMethod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`);
|
|
4165
|
+
let sinkIdx = -1;
|
|
4166
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
4167
|
+
if (methodRe.test(stmts[i])) {
|
|
4168
|
+
sinkIdx = i;
|
|
4169
|
+
break;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
if (sinkIdx <= 0)
|
|
4173
|
+
return false;
|
|
4174
|
+
const varAssign = new RegExp(`\\b${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=(?!=)`);
|
|
4175
|
+
for (let i = 0; i < sinkIdx; i++) {
|
|
4176
|
+
if (varAssign.test(stmts[i]))
|
|
4177
|
+
return true;
|
|
4178
|
+
}
|
|
4179
|
+
return false;
|
|
4180
|
+
};
|
|
2893
4181
|
// cognium-dev #83: inline-source colocation.
|
|
2894
4182
|
//
|
|
2895
4183
|
// When a source expression is used INLINE as a sink argument — e.g.
|
|
@@ -2978,6 +4266,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2978
4266
|
if (new RegExp(`\\b${sourceVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(rhs)) {
|
|
2979
4267
|
continue;
|
|
2980
4268
|
}
|
|
4269
|
+
if (boundInEarlierStatement(sinkCode, sourceVar, sink.method)) {
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
2981
4272
|
}
|
|
2982
4273
|
// Skip the degenerate `file_input` → `path_traversal` colocation
|
|
2983
4274
|
// where the source and sink describe the SAME call (one being the
|