circle-ir 4.14.2 → 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/const-table-lookup.d.ts +29 -0
- package/dist/analysis/const-table-lookup.d.ts.map +1 -0
- package/dist/analysis/const-table-lookup.js +145 -0
- package/dist/analysis/const-table-lookup.js.map +1 -0
- package/dist/analysis/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 +149 -4
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/go-strings-replacer.d.ts +21 -0
- package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
- package/dist/analysis/go-strings-replacer.js +145 -0
- package/dist/analysis/go-strings-replacer.js.map +1 -0
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +148 -0
- 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/java-canonical-prefix.d.ts +42 -0
- package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
- package/dist/analysis/java-canonical-prefix.js +145 -0
- package/dist/analysis/java-canonical-prefix.js.map +1 -0
- package/dist/analysis/java-crlf-strip.d.ts +30 -0
- package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/java-crlf-strip.js +195 -0
- package/dist/analysis/java-crlf-strip.js.map +1 -0
- package/dist/analysis/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/cleartext-credential-transport-pass.d.ts +2 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
- package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +43 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +127 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +20 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +76 -2
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +2045 -156
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +214 -6
- 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 +509 -27
- 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 +1606 -60
- 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 +33 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -0
- package/dist/analysis/scalar-derivation.js +368 -0
- package/dist/analysis/scalar-derivation.js.map +1 -0
- 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 +695 -34
- 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 +375 -24
- 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 +19 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +25539 -19200
- package/dist/core/circle-ir-core.cjs +3945 -2126
- package/dist/core/circle-ir-core.js +3945 -2126
- 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 +336 -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/graph/code-graph.d.ts +9 -0
- package/dist/graph/code-graph.d.ts.map +1 -1
- package/dist/graph/code-graph.js +11 -0
- package/dist/graph/code-graph.js.map +1 -1
- package/dist/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 +16 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +110 -42
- 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/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +3 -3
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { isKnown, createUnknown, getNodeText, getNodeLine } from './ast-utils.js';
|
|
8
8
|
import { ExpressionEvaluator } from './evaluator.js';
|
|
9
|
-
import { TAINT_PATTERN_REGEX, SANITIZER_METHODS, PROPAGATOR_METHODS, ANTI_SANITIZER_METHODS } from './patterns.js';
|
|
9
|
+
import { TAINT_PATTERN_REGEX, SANITIZER_METHODS, PROPAGATOR_METHODS, ANTI_SANITIZER_METHODS, HTML_CONTEXT_ENCODER_METHODS, HTML_CONTEXT_ENCODER_UNCOVERED_SINKS, } from './patterns.js';
|
|
10
|
+
import { javaArrayElementStorageKey } from '../java-array-element-keys.js';
|
|
10
11
|
/**
|
|
11
12
|
* Constant Propagator for taint analysis.
|
|
12
13
|
*
|
|
@@ -23,6 +24,8 @@ export class ConstantPropagator {
|
|
|
23
24
|
taintedCollections = new Map();
|
|
24
25
|
// Track variables explicitly assigned from sanitizer calls
|
|
25
26
|
sanitizedVars = new Set();
|
|
27
|
+
/** #589: per sanitized variable, the sink types its context-specific encoder does NOT clean. */
|
|
28
|
+
sanitizedVarUncovered = new Map();
|
|
26
29
|
source = '';
|
|
27
30
|
evaluator;
|
|
28
31
|
// Track the expression node that defined each variable (for refinement)
|
|
@@ -33,10 +36,23 @@ export class ConstantPropagator {
|
|
|
33
36
|
methodReturnsConstant = new Set();
|
|
34
37
|
// Track which methods always return sanitized values (inter-procedural analysis)
|
|
35
38
|
methodReturnsSanitized = new Set();
|
|
39
|
+
/** #581: same-file helpers named like a class-less sanitizer but not actually sanitizing. */
|
|
40
|
+
localNonSanitizing = new Set();
|
|
41
|
+
/**
|
|
42
|
+
* #589: same-file helpers whose every reachable return is a direct call to a
|
|
43
|
+
* context-specific HTML encoder (`return StringEscapeUtils.escapeHtml(v);`).
|
|
44
|
+
* Such a helper cleans `xss` like the encoder it wraps, but not `trust_boundary`.
|
|
45
|
+
*/
|
|
46
|
+
methodReturnsHtmlEncoderOnly = new Set();
|
|
47
|
+
methodNotHtmlEncoderOnly = new Set();
|
|
36
48
|
// Track which methods return a specific parameter (index) - for taint propagation
|
|
37
49
|
methodReturnsParameter = new Map();
|
|
38
50
|
// Track which methods return safe (non-tainted) values even with tainted input
|
|
39
51
|
methodReturnsSafeValue = new Set();
|
|
52
|
+
/** Go locals (method-scoped) that were ever assigned a value const-prop cannot prove clean (#751). */
|
|
53
|
+
goUnknownVars = new Set();
|
|
54
|
+
/** Every method-scoped name handleGoAssignment wrote; these get no unscoped alias in the result. */
|
|
55
|
+
goScopedNames = new Set();
|
|
40
56
|
// Additional taint patterns (for test harnesses to inject custom patterns)
|
|
41
57
|
additionalTaintPatterns = [];
|
|
42
58
|
// Track list elements by index for precise list taint tracking
|
|
@@ -103,13 +119,19 @@ export class ConstantPropagator {
|
|
|
103
119
|
this.definitionNodes.clear();
|
|
104
120
|
this.inConditionalBranch = false;
|
|
105
121
|
this.methodReturnsConstant.clear();
|
|
122
|
+
this.goUnknownVars.clear();
|
|
123
|
+
this.goScopedNames.clear();
|
|
106
124
|
this.methodReturnsSanitized.clear();
|
|
125
|
+
this.localNonSanitizing.clear();
|
|
126
|
+
this.methodReturnsHtmlEncoderOnly.clear();
|
|
127
|
+
this.methodNotHtmlEncoderOnly.clear();
|
|
107
128
|
this.methodReturnsParameter.clear();
|
|
108
129
|
this.methodReturnsSafeValue.clear();
|
|
109
130
|
this.listElements.clear();
|
|
110
131
|
this.loopVariables.clear();
|
|
111
132
|
this.taintedArrayElements.clear();
|
|
112
133
|
this.sanitizedVars.clear();
|
|
134
|
+
this.sanitizedVarUncovered.clear();
|
|
113
135
|
this.currentMethod = null;
|
|
114
136
|
this.conditionalTaints.clear();
|
|
115
137
|
this.conditionStack = [];
|
|
@@ -162,6 +184,11 @@ export class ConstantPropagator {
|
|
|
162
184
|
resultTainted.add(unscoped);
|
|
163
185
|
}
|
|
164
186
|
}
|
|
187
|
+
// #589 review: do NOT copy sanitizedVarUncovered onto the bare name.
|
|
188
|
+
// The unscoped sanitizedVars alias stays for legacy readers, but uncovered
|
|
189
|
+
// sink types are decided from the scoped `method:var` key only — otherwise
|
|
190
|
+
// a sibling method's local of the same name inherits the encoder set.
|
|
191
|
+
const resultScopes = new Map(this.sanitizedVarUncovered);
|
|
165
192
|
for (const name of this.sanitizedVars) {
|
|
166
193
|
if (name.includes(':')) {
|
|
167
194
|
const unscoped = name.substring(name.indexOf(':') + 1);
|
|
@@ -171,6 +198,11 @@ export class ConstantPropagator {
|
|
|
171
198
|
// Add unscoped symbols for backward compatibility with tests
|
|
172
199
|
// The scoped versions take priority in filterCleanVariableSinks
|
|
173
200
|
for (const [name, value] of this.symbols) {
|
|
201
|
+
// cognium-dev#751: a Go local is only meaningful in its own function. An
|
|
202
|
+
// unscoped alias would let `query := "SELECT 1"` in one function make
|
|
203
|
+
// every other function's `query` look clean to the veto.
|
|
204
|
+
if (this.goScopedNames.has(name))
|
|
205
|
+
continue;
|
|
174
206
|
if (name.includes(':')) {
|
|
175
207
|
const unscoped = name.substring(name.indexOf(':') + 1);
|
|
176
208
|
// Only add if not already present (scoped version wins on conflict)
|
|
@@ -186,10 +218,12 @@ export class ConstantPropagator {
|
|
|
186
218
|
taintedCollections: new Map(this.taintedCollections),
|
|
187
219
|
taintedArrayElements: new Map(this.taintedArrayElements),
|
|
188
220
|
sanitizedVars: resultSanitized,
|
|
221
|
+
sanitizedVarUncovered: resultScopes,
|
|
189
222
|
conditionalTaints: new Map(this.conditionalTaints),
|
|
190
223
|
lineConditions: new Map(this.lineConditions),
|
|
191
224
|
synchronizedLines: new Set(this.synchronizedLines),
|
|
192
225
|
instanceFieldTaint: new Map(this.instanceFieldTaint),
|
|
226
|
+
constantReturningMethods: new Set(this.methodReturnsConstant),
|
|
193
227
|
};
|
|
194
228
|
}
|
|
195
229
|
/**
|
|
@@ -259,6 +293,7 @@ export class ConstantPropagator {
|
|
|
259
293
|
let allReturnsConstant = true;
|
|
260
294
|
let allReturnsSanitized = true;
|
|
261
295
|
let allReturnsSafe = true; // Track if all returns are non-tainted
|
|
296
|
+
let allReturnsHtmlEncoder = true; // #589: every return is a direct HTML-encoder call
|
|
262
297
|
let returnedParamIndex = -1;
|
|
263
298
|
let hasReachableReturn = false;
|
|
264
299
|
for (const ret of returns) {
|
|
@@ -267,13 +302,23 @@ export class ConstantPropagator {
|
|
|
267
302
|
continue;
|
|
268
303
|
}
|
|
269
304
|
hasReachableReturn = true;
|
|
270
|
-
const valueNode =
|
|
305
|
+
const valueNode = this.returnExpressionValueNode(ret);
|
|
271
306
|
if (!valueNode) {
|
|
272
307
|
allReturnsConstant = false;
|
|
273
308
|
allReturnsSanitized = false;
|
|
309
|
+
allReturnsHtmlEncoder = false;
|
|
274
310
|
allReturnsSafe = false; // void return or unknown - not safe
|
|
275
311
|
continue;
|
|
276
312
|
}
|
|
313
|
+
// #589: `return <Encoder>.<encode>(<the only parameter>)`.
|
|
314
|
+
const encoderArgs = valueNode.type === 'method_invocation'
|
|
315
|
+
? valueNode.childForFieldName('arguments')?.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',')
|
|
316
|
+
: undefined;
|
|
317
|
+
if (!(valueNode.type === 'method_invocation' &&
|
|
318
|
+
HTML_CONTEXT_ENCODER_METHODS.has(getNodeText(valueNode.childForFieldName('name') ?? valueNode, this.source)) &&
|
|
319
|
+
params.length === 1 && encoderArgs?.length === 1 && getNodeText(encoderArgs[0], this.source) === params[0])) {
|
|
320
|
+
allReturnsHtmlEncoder = false;
|
|
321
|
+
}
|
|
277
322
|
const value = tempPropagator.evaluateExpression(valueNode);
|
|
278
323
|
if (!isKnown(value)) {
|
|
279
324
|
allReturnsConstant = false;
|
|
@@ -310,12 +355,25 @@ export class ConstantPropagator {
|
|
|
310
355
|
}
|
|
311
356
|
}
|
|
312
357
|
if (hasReachableReturn) {
|
|
358
|
+
// #589: an encoder-only helper is recorded only when no other method of
|
|
359
|
+
// this name (an overload, an annotated sanitizer) returns anything else.
|
|
360
|
+
if (allReturnsHtmlEncoder && allReturnsSanitized && !this.methodNotHtmlEncoderOnly.has(methodName) &&
|
|
361
|
+
(!this.methodReturnsSanitized.has(methodName) || this.methodReturnsHtmlEncoderOnly.has(methodName))) {
|
|
362
|
+
this.methodReturnsHtmlEncoderOnly.add(methodName);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
this.methodNotHtmlEncoderOnly.add(methodName);
|
|
366
|
+
this.methodReturnsHtmlEncoderOnly.delete(methodName);
|
|
367
|
+
}
|
|
313
368
|
if (allReturnsConstant) {
|
|
314
369
|
this.methodReturnsConstant.add(methodName);
|
|
315
370
|
}
|
|
316
371
|
if (allReturnsSanitized) {
|
|
317
372
|
this.methodReturnsSanitized.add(methodName);
|
|
318
373
|
}
|
|
374
|
+
if (hasReachableReturn && SANITIZER_METHODS.has(methodName) && !allReturnsSanitized) {
|
|
375
|
+
this.localNonSanitizing.add(methodName);
|
|
376
|
+
}
|
|
319
377
|
if (returnedParamIndex >= 0) {
|
|
320
378
|
this.methodReturnsParameter.set(methodName, returnedParamIndex);
|
|
321
379
|
}
|
|
@@ -431,6 +489,18 @@ export class ConstantPropagator {
|
|
|
431
489
|
}
|
|
432
490
|
return null;
|
|
433
491
|
}
|
|
492
|
+
/** Return the expression node from a `return_statement` (Java, Go, Python, …). */
|
|
493
|
+
returnExpressionValueNode(ret) {
|
|
494
|
+
const byField = ret.childForFieldName('result');
|
|
495
|
+
if (byField)
|
|
496
|
+
return byField;
|
|
497
|
+
const candidates = ret.children.filter((c) => c.type !== 'return' && c.type !== ';' && c.type !== 'comment');
|
|
498
|
+
let valueNode = candidates[0];
|
|
499
|
+
if (valueNode?.type === 'expression_list') {
|
|
500
|
+
valueNode = valueNode.children.find((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
|
|
501
|
+
}
|
|
502
|
+
return valueNode;
|
|
503
|
+
}
|
|
434
504
|
getMethodParameters(method) {
|
|
435
505
|
const params = [];
|
|
436
506
|
const paramsNode = method.childForFieldName('parameters');
|
|
@@ -443,6 +513,20 @@ export class ConstantPropagator {
|
|
|
443
513
|
params.push(getNodeText(nameNode, this.source));
|
|
444
514
|
}
|
|
445
515
|
}
|
|
516
|
+
else if (child.type === 'parameter_declaration') {
|
|
517
|
+
const nameNode = child.childForFieldName('name');
|
|
518
|
+
if (nameNode) {
|
|
519
|
+
if (nameNode.type === 'identifier') {
|
|
520
|
+
params.push(getNodeText(nameNode, this.source));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
for (const id of nameNode.children) {
|
|
524
|
+
if (id.type === 'identifier')
|
|
525
|
+
params.push(getNodeText(id, this.source));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
446
530
|
}
|
|
447
531
|
return params;
|
|
448
532
|
}
|
|
@@ -451,7 +535,7 @@ export class ConstantPropagator {
|
|
|
451
535
|
const nameNode = node.childForFieldName('name');
|
|
452
536
|
if (nameNode) {
|
|
453
537
|
const methodName = getNodeText(nameNode, this.source);
|
|
454
|
-
if (SANITIZER_METHODS.has(methodName)) {
|
|
538
|
+
if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
|
|
455
539
|
return true;
|
|
456
540
|
}
|
|
457
541
|
}
|
|
@@ -832,6 +916,7 @@ export class ConstantPropagator {
|
|
|
832
916
|
}
|
|
833
917
|
switch (node.type) {
|
|
834
918
|
case 'method_declaration':
|
|
919
|
+
case 'function_declaration':
|
|
835
920
|
case 'constructor_declaration':
|
|
836
921
|
this.handleMethodDeclaration(node);
|
|
837
922
|
return true; // Don't visit children directly, handleMethodDeclaration does it
|
|
@@ -859,6 +944,10 @@ export class ConstantPropagator {
|
|
|
859
944
|
case 'assignment_expression':
|
|
860
945
|
this.handleAssignment(node);
|
|
861
946
|
return false;
|
|
947
|
+
case 'short_var_declaration':
|
|
948
|
+
case 'assignment_statement':
|
|
949
|
+
this.handleGoAssignment(node);
|
|
950
|
+
return false;
|
|
862
951
|
case 'update_expression':
|
|
863
952
|
this.handleUpdateExpression(node);
|
|
864
953
|
return false;
|
|
@@ -884,6 +973,10 @@ export class ConstantPropagator {
|
|
|
884
973
|
case 'synchronized_statement':
|
|
885
974
|
this.handleSynchronizedStatement(node);
|
|
886
975
|
return true;
|
|
976
|
+
case 'try_statement':
|
|
977
|
+
case 'try_with_resources_statement':
|
|
978
|
+
this.handleTryStatement(node);
|
|
979
|
+
return true;
|
|
887
980
|
default:
|
|
888
981
|
return false;
|
|
889
982
|
}
|
|
@@ -1093,6 +1186,59 @@ export class ConstantPropagator {
|
|
|
1093
1186
|
this.collectLoopVariableNames(child, names);
|
|
1094
1187
|
}
|
|
1095
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* cognium-dev#753 — try/catch must not let a catch-arm literal assignment
|
|
1191
|
+
* erase taint established on the normal-completion path through the try body
|
|
1192
|
+
* (`try { label = format(picked); } catch (…) { label = ""; }` then
|
|
1193
|
+
* `exec(tool + label)`).
|
|
1194
|
+
*/
|
|
1195
|
+
handleTryStatement(node) {
|
|
1196
|
+
const body = node.childForFieldName('body');
|
|
1197
|
+
const catches = node.children.filter((c) => c.type === 'catch_clause');
|
|
1198
|
+
const finallyClause = node.children.find((c) => c.type === 'finally_clause');
|
|
1199
|
+
const taintedBefore = new Set(this.tainted);
|
|
1200
|
+
const symbolsBefore = new Map(this.symbols);
|
|
1201
|
+
if (body)
|
|
1202
|
+
this.visit(body);
|
|
1203
|
+
const taintedAfterTry = new Set(this.tainted);
|
|
1204
|
+
const symbolsAfterTry = new Map(this.symbols);
|
|
1205
|
+
this.tainted = new Set(taintedBefore);
|
|
1206
|
+
this.symbols = new Map(symbolsBefore);
|
|
1207
|
+
for (const c of catches)
|
|
1208
|
+
this.visit(c);
|
|
1209
|
+
const taintedAfterCatch = new Set(this.tainted);
|
|
1210
|
+
const symbolsAfterCatch = new Map(this.symbols);
|
|
1211
|
+
this.tainted = new Set([...taintedBefore, ...taintedAfterTry, ...taintedAfterCatch]);
|
|
1212
|
+
this.symbols = new Map(symbolsBefore);
|
|
1213
|
+
const keys = new Set([...symbolsAfterTry.keys(), ...symbolsAfterCatch.keys()]);
|
|
1214
|
+
for (const key of keys) {
|
|
1215
|
+
const merged = this.mergeTryCatchSymbol(symbolsAfterTry.get(key), symbolsAfterCatch.get(key));
|
|
1216
|
+
if (merged)
|
|
1217
|
+
this.symbols.set(key, merged);
|
|
1218
|
+
}
|
|
1219
|
+
if (finallyClause)
|
|
1220
|
+
this.visit(finallyClause);
|
|
1221
|
+
}
|
|
1222
|
+
/** Prefer unknown / non-literal over a definite literal when paths disagree. */
|
|
1223
|
+
mergeTryCatchSymbol(tryVal, catchVal) {
|
|
1224
|
+
if (!tryVal && !catchVal)
|
|
1225
|
+
return undefined;
|
|
1226
|
+
if (!tryVal)
|
|
1227
|
+
return catchVal;
|
|
1228
|
+
if (!catchVal)
|
|
1229
|
+
return tryVal;
|
|
1230
|
+
if (!isKnown(tryVal) || tryVal.type === 'unknown')
|
|
1231
|
+
return tryVal;
|
|
1232
|
+
if (!isKnown(catchVal) || catchVal.type === 'unknown')
|
|
1233
|
+
return catchVal;
|
|
1234
|
+
if (tryVal.type !== catchVal.type) {
|
|
1235
|
+
return createUnknown(Math.max(tryVal.sourceLine ?? 0, catchVal.sourceLine ?? 0));
|
|
1236
|
+
}
|
|
1237
|
+
if (tryVal.type === 'string' && catchVal.type === 'string' && tryVal.value !== catchVal.value) {
|
|
1238
|
+
return createUnknown(Math.max(tryVal.sourceLine ?? 0, catchVal.sourceLine ?? 0));
|
|
1239
|
+
}
|
|
1240
|
+
return tryVal;
|
|
1241
|
+
}
|
|
1096
1242
|
/**
|
|
1097
1243
|
* Handle synchronized statements.
|
|
1098
1244
|
* Operations inside synchronized blocks are atomic, so field strong updates are safe.
|
|
@@ -1170,6 +1316,7 @@ export class ConstantPropagator {
|
|
|
1170
1316
|
if (isTainted) {
|
|
1171
1317
|
this.tainted.add(scopedName);
|
|
1172
1318
|
this.sanitizedVars.delete(scopedName); // No longer sanitized if receiving tainted value
|
|
1319
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1173
1320
|
this.definitionNodes.set(scopedName, valueNode);
|
|
1174
1321
|
this.symbols.set(scopedName, createUnknown(line));
|
|
1175
1322
|
}
|
|
@@ -1182,15 +1329,23 @@ export class ConstantPropagator {
|
|
|
1182
1329
|
this.definitionNodes.delete(scopedName);
|
|
1183
1330
|
const value = this.evaluateExpression(valueNode);
|
|
1184
1331
|
this.symbols.set(scopedName, value);
|
|
1185
|
-
// Track if this variable was explicitly assigned from a sanitizer call
|
|
1332
|
+
// Track if this variable was explicitly assigned from a sanitizer call.
|
|
1333
|
+
// A constant / non-sanitizer overwrite must clear any prior encoder
|
|
1334
|
+
// scope (#589 review: `data = escapeHtml(x); data = "safe"`).
|
|
1186
1335
|
if (this.isSanitizerMethodCall(valueNode)) {
|
|
1187
1336
|
this.sanitizedVars.add(scopedName);
|
|
1337
|
+
this.recordSanitizerScope(scopedName, valueNode);
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
this.sanitizedVars.delete(scopedName);
|
|
1341
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1188
1342
|
}
|
|
1189
1343
|
// Check if this is an anti-sanitizer call that reintroduces taint
|
|
1190
1344
|
// e.g., URLDecoder.decode(sanitizedVar) produces tainted output
|
|
1191
1345
|
if (this.antiSanitizerReintroducesTaint(valueNode)) {
|
|
1192
1346
|
this.tainted.add(scopedName);
|
|
1193
1347
|
this.sanitizedVars.delete(scopedName);
|
|
1348
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1194
1349
|
}
|
|
1195
1350
|
}
|
|
1196
1351
|
}
|
|
@@ -1201,6 +1356,101 @@ export class ConstantPropagator {
|
|
|
1201
1356
|
}
|
|
1202
1357
|
}
|
|
1203
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Go `:=` / `=` (cognium-dev#751).
|
|
1361
|
+
*
|
|
1362
|
+
* Const-prop does not model Go request sources, so a right-hand side it does
|
|
1363
|
+
* not see as tainted is usually just *unknown* (`r.URL.Query().Get("f")`,
|
|
1364
|
+
* `"SELECT " + f`, `r.FormValue("x")`). Recording such a variable as a clean
|
|
1365
|
+
* symbol makes the const-prop veto drop real flows downstream, so only a
|
|
1366
|
+
* right-hand side that is provably clean — a constant, or a call to a local
|
|
1367
|
+
* helper that always returns a constant — clears the variable. Anything else stops being tracked here, which is what Go
|
|
1368
|
+
* assignments got before this handler existed: the DFG decides.
|
|
1369
|
+
*/
|
|
1370
|
+
handleGoAssignment(node) {
|
|
1371
|
+
const left = node.childForFieldName('left');
|
|
1372
|
+
const right = node.childForFieldName('right');
|
|
1373
|
+
if (!left || !right)
|
|
1374
|
+
return;
|
|
1375
|
+
const names = [];
|
|
1376
|
+
if (left.type === 'identifier') {
|
|
1377
|
+
names.push(getNodeText(left, this.source));
|
|
1378
|
+
}
|
|
1379
|
+
else if (left.type === 'expression_list') {
|
|
1380
|
+
for (const c of left.children) {
|
|
1381
|
+
if (c.type === 'identifier')
|
|
1382
|
+
names.push(getNodeText(c, this.source));
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (names.length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
// The Go grammar wraps every right-hand side in an `expression_list`, so
|
|
1388
|
+
// pair each name with its own expression (`a, b := x, y`); one expression
|
|
1389
|
+
// for several names (`v, err := f()`) applies to all of them.
|
|
1390
|
+
const rhs = right.type === 'expression_list'
|
|
1391
|
+
? right.children.filter((c) => c.type !== ',' && c.type !== 'comment')
|
|
1392
|
+
: [right];
|
|
1393
|
+
const line = getNodeLine(node);
|
|
1394
|
+
for (let i = 0; i < names.length; i++) {
|
|
1395
|
+
const varName = names[i];
|
|
1396
|
+
if (varName === '_')
|
|
1397
|
+
continue;
|
|
1398
|
+
const value = rhs.length === names.length ? rhs[i] : rhs.length === 1 ? rhs[0] : undefined;
|
|
1399
|
+
const scopedName = this.getScopedName(varName);
|
|
1400
|
+
this.goScopedNames.add(scopedName);
|
|
1401
|
+
if (value && this.isTaintedExpression(value)) {
|
|
1402
|
+
this.goUnknownVars.add(scopedName);
|
|
1403
|
+
this.tainted.add(scopedName);
|
|
1404
|
+
this.sanitizedVars.delete(scopedName);
|
|
1405
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1406
|
+
this.definitionNodes.set(scopedName, value);
|
|
1407
|
+
this.symbols.set(scopedName, createUnknown(line));
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (!value || !this.isGoKnownCleanRhs(value) || this.goUnknownVars.has(scopedName)) {
|
|
1411
|
+
// Unknown value: forget what we knew, and do not trust a later clean
|
|
1412
|
+
// assignment either, since const-prop's symbols are not flow-sensitive
|
|
1413
|
+
// and a sink between the two would be vetoed. In a conditional branch
|
|
1414
|
+
// the other path may still carry the old value, so keep any taint.
|
|
1415
|
+
this.goUnknownVars.add(scopedName);
|
|
1416
|
+
this.symbols.delete(scopedName);
|
|
1417
|
+
this.sanitizedVars.delete(scopedName);
|
|
1418
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1419
|
+
if (!this.inConditionalBranch) {
|
|
1420
|
+
this.tainted.delete(scopedName);
|
|
1421
|
+
this.definitionNodes.delete(scopedName);
|
|
1422
|
+
}
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
// Provably clean, but only on this path: a branch must not clear taint
|
|
1426
|
+
// or make an untracked variable look clean at the join.
|
|
1427
|
+
if (this.inConditionalBranch) {
|
|
1428
|
+
this.goUnknownVars.add(scopedName);
|
|
1429
|
+
this.symbols.delete(scopedName);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
this.tainted.delete(scopedName);
|
|
1433
|
+
this.definitionNodes.delete(scopedName);
|
|
1434
|
+
this.symbols.set(scopedName, this.evaluateExpression(value));
|
|
1435
|
+
this.sanitizedVars.delete(scopedName);
|
|
1436
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* A Go right-hand side whose value is clean regardless of its inputs: a
|
|
1441
|
+
* compile-time constant, or a call to a local helper whose every reachable
|
|
1442
|
+
* return is a constant (#751). Sanitizer-style calls are deliberately not
|
|
1443
|
+
* included: clearing here vetoes every sink type, and Go sanitizers are
|
|
1444
|
+
* sink-specific (`strconv.Atoi` bounds nothing for a `make` size, #349).
|
|
1445
|
+
*/
|
|
1446
|
+
isGoKnownCleanRhs(right) {
|
|
1447
|
+
if (isKnown(this.evaluateExpression(right)))
|
|
1448
|
+
return true;
|
|
1449
|
+
if (right.type !== 'call_expression')
|
|
1450
|
+
return false;
|
|
1451
|
+
const callee = this.extractCallName(right);
|
|
1452
|
+
return callee !== null && this.methodReturnsConstant.has(callee);
|
|
1453
|
+
}
|
|
1204
1454
|
handleAssignment(node) {
|
|
1205
1455
|
const left = node.childForFieldName('left');
|
|
1206
1456
|
const right = node.childForFieldName('right');
|
|
@@ -1225,6 +1475,7 @@ export class ConstantPropagator {
|
|
|
1225
1475
|
if (this.isTaintedExpression(right)) {
|
|
1226
1476
|
this.tainted.add(scopedName);
|
|
1227
1477
|
this.sanitizedVars.delete(scopedName); // No longer sanitized if receiving tainted value
|
|
1478
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1228
1479
|
this.definitionNodes.set(scopedName, right);
|
|
1229
1480
|
this.symbols.set(scopedName, createUnknown(line));
|
|
1230
1481
|
// Track constructor field assignments: this.field = taintedParam
|
|
@@ -1269,9 +1520,15 @@ export class ConstantPropagator {
|
|
|
1269
1520
|
this.definitionNodes.delete(scopedName);
|
|
1270
1521
|
const value = this.evaluateExpression(right);
|
|
1271
1522
|
this.symbols.set(scopedName, value);
|
|
1272
|
-
// Track if this variable was explicitly assigned from a sanitizer call
|
|
1523
|
+
// Track if this variable was explicitly assigned from a sanitizer call.
|
|
1524
|
+
// Constant / non-sanitizer overwrite clears prior encoder scope (#589).
|
|
1273
1525
|
if (this.isSanitizerMethodCall(right)) {
|
|
1274
1526
|
this.sanitizedVars.add(scopedName);
|
|
1527
|
+
this.recordSanitizerScope(scopedName, right);
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
this.sanitizedVars.delete(scopedName);
|
|
1531
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1275
1532
|
}
|
|
1276
1533
|
}
|
|
1277
1534
|
// Check if this is an anti-sanitizer call that reintroduces taint
|
|
@@ -1279,17 +1536,24 @@ export class ConstantPropagator {
|
|
|
1279
1536
|
if (this.antiSanitizerReintroducesTaint(right)) {
|
|
1280
1537
|
this.tainted.add(scopedName);
|
|
1281
1538
|
this.sanitizedVars.delete(scopedName);
|
|
1539
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
1282
1540
|
}
|
|
1283
1541
|
}
|
|
1284
1542
|
}
|
|
1285
|
-
handleArrayElementAssignment(left, right,
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const indexNode = left.childForFieldName('index') || left.child(2);
|
|
1289
|
-
if (!arrayNode)
|
|
1543
|
+
handleArrayElementAssignment(left, right, _node) {
|
|
1544
|
+
const chain = this.parseArrayAccessChain(left);
|
|
1545
|
+
if (!chain)
|
|
1290
1546
|
return;
|
|
1291
|
-
const arrayName =
|
|
1292
|
-
|
|
1547
|
+
const { arrayName, indexKey } = this.arrayElementStorageKey(chain);
|
|
1548
|
+
const isTainted = this.isTaintedExpression(right);
|
|
1549
|
+
this.setArrayElementTaint(arrayName, indexKey, isTainted);
|
|
1550
|
+
}
|
|
1551
|
+
/** cognium-dev#476: `grid[0][0]` and deeper element paths. */
|
|
1552
|
+
parseArrayAccessChain(node) {
|
|
1553
|
+
if (node.type !== 'array_access' && node.type !== 'subscript_expression') {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
const indexNode = node.childForFieldName('index') || node.child(2);
|
|
1293
1557
|
let indexKey = '*';
|
|
1294
1558
|
if (indexNode) {
|
|
1295
1559
|
const indexValue = this.evaluateExpression(indexNode);
|
|
@@ -1297,24 +1561,54 @@ export class ConstantPropagator {
|
|
|
1297
1561
|
indexKey = String(indexValue.value);
|
|
1298
1562
|
}
|
|
1299
1563
|
}
|
|
1300
|
-
const
|
|
1564
|
+
const arrayNode = node.childForFieldName('array') || node.child(0);
|
|
1565
|
+
if (!arrayNode)
|
|
1566
|
+
return null;
|
|
1567
|
+
if (arrayNode.type === 'identifier') {
|
|
1568
|
+
return { base: getNodeText(arrayNode, this.source), indices: [indexKey] };
|
|
1569
|
+
}
|
|
1570
|
+
if (arrayNode.type === 'array_access' || arrayNode.type === 'subscript_expression') {
|
|
1571
|
+
const inner = this.parseArrayAccessChain(arrayNode);
|
|
1572
|
+
if (!inner)
|
|
1573
|
+
return null;
|
|
1574
|
+
return { base: inner.base, indices: [...inner.indices, indexKey] };
|
|
1575
|
+
}
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
arrayElementStorageKey(chain) {
|
|
1579
|
+
return javaArrayElementStorageKey(chain.base, chain.indices);
|
|
1580
|
+
}
|
|
1581
|
+
setArrayElementTaint(arrayName, indexKey, isTainted) {
|
|
1301
1582
|
if (isTainted) {
|
|
1302
|
-
// Mark this array element as tainted
|
|
1303
1583
|
if (!this.taintedArrayElements.has(arrayName)) {
|
|
1304
1584
|
this.taintedArrayElements.set(arrayName, new Set());
|
|
1305
1585
|
}
|
|
1306
1586
|
this.taintedArrayElements.get(arrayName).add(indexKey);
|
|
1307
1587
|
}
|
|
1308
1588
|
else {
|
|
1309
|
-
// Mark this array element as clean (remove from tainted set)
|
|
1310
1589
|
const taintedIndices = this.taintedArrayElements.get(arrayName);
|
|
1311
1590
|
if (taintedIndices) {
|
|
1312
1591
|
taintedIndices.delete(indexKey);
|
|
1313
|
-
// If we're assigning to a specific index and '*' is in the set,
|
|
1314
|
-
// we can't remove '*' because other indices might still be tainted
|
|
1315
1592
|
}
|
|
1316
1593
|
}
|
|
1317
1594
|
}
|
|
1595
|
+
isArrayElementAccessTainted(chain) {
|
|
1596
|
+
if (this.tainted.has(chain.base)) {
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
const { arrayName, indexKey } = this.arrayElementStorageKey(chain);
|
|
1600
|
+
const taintedIndices = this.taintedArrayElements.get(arrayName);
|
|
1601
|
+
if (!taintedIndices) {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
if (taintedIndices.has('*')) {
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
if (indexKey === '*') {
|
|
1608
|
+
return taintedIndices.size > 0;
|
|
1609
|
+
}
|
|
1610
|
+
return taintedIndices.has(indexKey);
|
|
1611
|
+
}
|
|
1318
1612
|
handleUpdateExpression(node) {
|
|
1319
1613
|
// Handle x++, ++x, x--, --x
|
|
1320
1614
|
// The operand is a positional child (identifier), not a named field
|
|
@@ -1456,10 +1750,12 @@ export class ConstantPropagator {
|
|
|
1456
1750
|
if (guardedVar) {
|
|
1457
1751
|
this.tainted.delete(guardedVar);
|
|
1458
1752
|
this.sanitizedVars.add(guardedVar);
|
|
1753
|
+
this.sanitizedVarUncovered.delete(guardedVar);
|
|
1459
1754
|
const scoped = this.getScopedName(guardedVar);
|
|
1460
1755
|
if (scoped !== guardedVar) {
|
|
1461
1756
|
this.tainted.delete(scoped);
|
|
1462
1757
|
this.sanitizedVars.add(scoped);
|
|
1758
|
+
this.sanitizedVarUncovered.delete(scoped);
|
|
1463
1759
|
}
|
|
1464
1760
|
}
|
|
1465
1761
|
}
|
|
@@ -1682,6 +1978,7 @@ export class ConstantPropagator {
|
|
|
1682
1978
|
for (const child of node.children) {
|
|
1683
1979
|
if (child.type === 'method_invocation') {
|
|
1684
1980
|
this.checkCollectionTaint(child);
|
|
1981
|
+
this.handleSystemArrayCopy(child);
|
|
1685
1982
|
}
|
|
1686
1983
|
}
|
|
1687
1984
|
for (const child of node.children) {
|
|
@@ -1772,11 +2069,27 @@ export class ConstantPropagator {
|
|
|
1772
2069
|
* Handles Java (`method_invocation`), Go/JS/TS (`call_expression`), and
|
|
1773
2070
|
* Python (`call`) AST shapes.
|
|
1774
2071
|
*/
|
|
2072
|
+
/**
|
|
2073
|
+
* #589: remember which sink types the sanitizer behind `scopedName` does NOT
|
|
2074
|
+
* clean. Only context-specific HTML/markup encoders carry an entry; any other
|
|
2075
|
+
* sanitizer clears a previously recorded one (the variable was reassigned).
|
|
2076
|
+
*/
|
|
2077
|
+
recordSanitizerScope(scopedName, node) {
|
|
2078
|
+
const methodName = this.extractCallName(node);
|
|
2079
|
+
if (methodName && ((HTML_CONTEXT_ENCODER_METHODS.has(methodName) && !this.methodReturnsSanitized.has(methodName)) ||
|
|
2080
|
+
this.methodReturnsHtmlEncoderOnly.has(methodName))) {
|
|
2081
|
+
this.sanitizedVarUncovered.set(scopedName, HTML_CONTEXT_ENCODER_UNCOVERED_SINKS);
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
this.sanitizedVarUncovered.delete(scopedName);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1775
2087
|
isSanitizerMethodCall(node) {
|
|
1776
2088
|
const methodName = this.extractCallName(node);
|
|
1777
2089
|
if (!methodName)
|
|
1778
2090
|
return false;
|
|
1779
|
-
return SANITIZER_METHODS.has(methodName)
|
|
2091
|
+
return (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName))
|
|
2092
|
+
|| this.methodReturnsSanitized.has(methodName);
|
|
1780
2093
|
}
|
|
1781
2094
|
/**
|
|
1782
2095
|
* Extract the trailing method/function name from any call node shape:
|
|
@@ -2013,7 +2326,7 @@ export class ConstantPropagator {
|
|
|
2013
2326
|
const objectNode = node.childForFieldName('object');
|
|
2014
2327
|
if (nameNode) {
|
|
2015
2328
|
const methodName = getNodeText(nameNode, this.source);
|
|
2016
|
-
if (SANITIZER_METHODS.has(methodName)) {
|
|
2329
|
+
if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
|
|
2017
2330
|
return false;
|
|
2018
2331
|
}
|
|
2019
2332
|
if (this.methodReturnsConstant.has(methodName)) {
|
|
@@ -2177,43 +2490,50 @@ export class ConstantPropagator {
|
|
|
2177
2490
|
}
|
|
2178
2491
|
}
|
|
2179
2492
|
}
|
|
2180
|
-
if (node.type === '
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
// Check if the whole array is tainted
|
|
2186
|
-
if (arrayNode.type === 'identifier' && this.tainted.has(arrayName)) {
|
|
2187
|
-
return true;
|
|
2493
|
+
if (node.type === 'call_expression' || node.type === 'call') {
|
|
2494
|
+
const methodName = this.extractCallName(node);
|
|
2495
|
+
if (methodName) {
|
|
2496
|
+
if (SANITIZER_METHODS.has(methodName) && !this.localNonSanitizing.has(methodName)) {
|
|
2497
|
+
return false;
|
|
2188
2498
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
// Specific index is NOT tainted - return false for this access
|
|
2205
|
-
return false;
|
|
2206
|
-
}
|
|
2207
|
-
// Unknown index - check if ANY element is tainted
|
|
2208
|
-
if (taintedIndices.size > 0) {
|
|
2499
|
+
if (this.methodReturnsConstant.has(methodName)) {
|
|
2500
|
+
return false;
|
|
2501
|
+
}
|
|
2502
|
+
if (this.methodReturnsSanitized.has(methodName)) {
|
|
2503
|
+
return false;
|
|
2504
|
+
}
|
|
2505
|
+
if (this.methodReturnsSafeValue.has(methodName)) {
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
const returnedParamIdx = this.methodReturnsParameter.get(methodName);
|
|
2509
|
+
if (returnedParamIdx !== undefined && returnedParamIdx >= 0) {
|
|
2510
|
+
const argsNode = node.childForFieldName('arguments');
|
|
2511
|
+
if (argsNode) {
|
|
2512
|
+
const args = argsNode.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
|
|
2513
|
+
if (args.length > returnedParamIdx && this.isTaintedExpression(args[returnedParamIdx])) {
|
|
2209
2514
|
return true;
|
|
2210
2515
|
}
|
|
2211
2516
|
}
|
|
2212
2517
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
if (node.type === 'array_access' || node.type === 'subscript_expression') {
|
|
2521
|
+
const chain = this.parseArrayAccessChain(node);
|
|
2522
|
+
if (chain) {
|
|
2523
|
+
if (this.isArrayElementAccessTainted(chain)) {
|
|
2215
2524
|
return true;
|
|
2216
2525
|
}
|
|
2526
|
+
if (chain.indices.length === 1 && chain.indices[0] !== '*') {
|
|
2527
|
+
// Known 1-D index with no element taint recorded — not tainted.
|
|
2528
|
+
const taintedIndices = this.taintedArrayElements.get(chain.base);
|
|
2529
|
+
if (taintedIndices && !this.tainted.has(chain.base)) {
|
|
2530
|
+
return false;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const arrayNode = node.childForFieldName('array') || node.child(0);
|
|
2535
|
+
if (arrayNode && this.isTaintedExpression(arrayNode)) {
|
|
2536
|
+
return true;
|
|
2217
2537
|
}
|
|
2218
2538
|
}
|
|
2219
2539
|
if (node.type === 'ternary_expression') {
|
|
@@ -2318,6 +2638,33 @@ export class ConstantPropagator {
|
|
|
2318
2638
|
// No verdict at this node — let the wrapper descend into children.
|
|
2319
2639
|
return undefined;
|
|
2320
2640
|
}
|
|
2641
|
+
/** cognium-dev#476: copy tainted array element indices across `System.arraycopy`. */
|
|
2642
|
+
handleSystemArrayCopy(node) {
|
|
2643
|
+
const objectNode = node.childForFieldName('object');
|
|
2644
|
+
const nameNode = node.childForFieldName('name');
|
|
2645
|
+
const argsNode = node.childForFieldName('arguments');
|
|
2646
|
+
if (!nameNode || !argsNode)
|
|
2647
|
+
return;
|
|
2648
|
+
if (getNodeText(nameNode, this.source) !== 'arraycopy')
|
|
2649
|
+
return;
|
|
2650
|
+
if (!objectNode || getNodeText(objectNode, this.source) !== 'System')
|
|
2651
|
+
return;
|
|
2652
|
+
const args = argsNode.children.filter((c) => c.type !== '(' && c.type !== ')' && c.type !== ',');
|
|
2653
|
+
if (args.length < 3)
|
|
2654
|
+
return;
|
|
2655
|
+
const srcName = getNodeText(args[0], this.source);
|
|
2656
|
+
const dstName = getNodeText(args[2], this.source);
|
|
2657
|
+
const srcTainted = this.taintedArrayElements.get(srcName);
|
|
2658
|
+
if (!srcTainted || srcTainted.size === 0)
|
|
2659
|
+
return;
|
|
2660
|
+
if (!this.taintedArrayElements.has(dstName)) {
|
|
2661
|
+
this.taintedArrayElements.set(dstName, new Set());
|
|
2662
|
+
}
|
|
2663
|
+
const dstSet = this.taintedArrayElements.get(dstName);
|
|
2664
|
+
for (const idx of srcTainted) {
|
|
2665
|
+
dstSet.add(idx);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2321
2668
|
checkCollectionTaint(node) {
|
|
2322
2669
|
const objectNode = node.childForFieldName('object');
|
|
2323
2670
|
const nameNode = node.childForFieldName('name');
|