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
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { DOM_ASSIGNMENT_SINK_METHODS, isProvablyLiteralExpression, } from '../literal-expression.js';
|
|
17
17
|
import { attachSourceLineCode } from '../taint-matcher.js';
|
|
18
|
+
import { javaReplaceCrLfCoverage } from '../java-crlf-strip.js';
|
|
19
|
+
import { javaCallArgument, javaCanonicalBaseLacksSeparator } from '../java-canonical-prefix.js';
|
|
20
|
+
import { findGoStringsReplacerSanitizers } from '../go-strings-replacer.js';
|
|
21
|
+
import { findRustLiteralEqualityGuardSanitizers } from '../rust-literal-guard.js';
|
|
22
|
+
import { constLookupTables, isConstTableLookup } from '../const-table-lookup.js';
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Pattern tables (moved from analyzer.ts)
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
@@ -218,6 +223,22 @@ export class LanguageSourcesPass {
|
|
|
218
223
|
});
|
|
219
224
|
}
|
|
220
225
|
}
|
|
226
|
+
// -- Java (#624): JSPWiki plugin `execute` return is HTML output ------------
|
|
227
|
+
if (language === 'java') {
|
|
228
|
+
const tm = ctx.getResult('taint-matcher');
|
|
229
|
+
const xssSan = tm.sanitizers.filter(sa => sa.sanitizes.includes('xss')).map(sa => ({ line: sa.line, method: sa.method }));
|
|
230
|
+
for (const r of findJavaWikiPluginReturnXSSSinks(code, types, graph.ir.imports, tm.sources, xssSan)) {
|
|
231
|
+
if (additionalSinks.some(s => s.line === r.sinkLine && s.type === 'xss'))
|
|
232
|
+
continue;
|
|
233
|
+
additionalSinks.push({
|
|
234
|
+
type: 'xss',
|
|
235
|
+
cwe: 'CWE-79',
|
|
236
|
+
line: r.sinkLine,
|
|
237
|
+
location: `WikiPlugin.execute returns HTML with user input at line ${r.sinkLine}`,
|
|
238
|
+
confidence: 0.9,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
221
242
|
// -- Python: assignment sources, trust-boundary sinks, return-XSS sinks --
|
|
222
243
|
additionalSources.push(...findPythonAssignmentSources(code, language));
|
|
223
244
|
const pyTaintedVars = language === 'python' ? buildPythonTaintedVars(code) : new Map();
|
|
@@ -285,13 +306,19 @@ export class LanguageSourcesPass {
|
|
|
285
306
|
additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
|
|
286
307
|
additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
|
|
287
308
|
additionalSanitizers.push(...findGoPathContainmentGuardSanitizers(code));
|
|
309
|
+
// #348 / #293: strings.NewReplacer CR/LF strip and < > entity escape.
|
|
310
|
+
additionalSanitizers.push(...findGoStringsReplacerSanitizers(code));
|
|
288
311
|
// Sprint 78 (#190): Go ECB-mode weak-crypto detection.
|
|
289
312
|
const goMisconfigFindings = findGoPatternFindings(code, graph.ir.meta.file);
|
|
290
313
|
for (const finding of goMisconfigFindings) {
|
|
291
314
|
ctx.addFinding(finding);
|
|
292
315
|
}
|
|
293
316
|
// Sprint 81 (#189): Go xss — fmt.Fprint(f|ln) to http.ResponseWriter.
|
|
294
|
-
|
|
317
|
+
// #568: an arg that is an int/bool measure local carries no payload.
|
|
318
|
+
const scalarVarsAt = (line) => new Set(graph.usesAtLine(line)
|
|
319
|
+
.filter(u => u.def_id != null && graph.taintBarrierDefIds.has(u.def_id))
|
|
320
|
+
.map(u => u.variable));
|
|
321
|
+
for (const finding of findGoXssFindings(code, graph.ir.meta.file, scalarVarsAt)) {
|
|
295
322
|
ctx.addFinding(finding);
|
|
296
323
|
}
|
|
297
324
|
// Sprint 82 (#189): Go open_redirect — Header().Set("Location", taint).
|
|
@@ -332,6 +359,7 @@ export class LanguageSourcesPass {
|
|
|
332
359
|
// recognition.
|
|
333
360
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
334
361
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
362
|
+
additionalSanitizers.push(...findPythonLiteralMembershipGuardSanitizers(code));
|
|
335
363
|
additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
|
|
336
364
|
additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
|
|
337
365
|
additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
|
|
@@ -391,6 +419,8 @@ export class LanguageSourcesPass {
|
|
|
391
419
|
if (language === 'rust') {
|
|
392
420
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
393
421
|
additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
|
|
422
|
+
// cognium-dev#293: `if x != "a" && x != "b" { return Err(..) }` literal-equality guard.
|
|
423
|
+
additionalSanitizers.push(...findRustLiteralEqualityGuardSanitizers(code));
|
|
394
424
|
// Sprint 77a (#216 Pattern X): argv-form Command::new(literal).arg(...)
|
|
395
425
|
// sanitizer.
|
|
396
426
|
additionalSanitizers.push(...findRustArgvCommandSanitizers(code));
|
|
@@ -523,6 +553,26 @@ export class LanguageSourcesPass {
|
|
|
523
553
|
// cognium-dev#269: `!file.getCanonicalPath().startsWith(base)` reject
|
|
524
554
|
// guard (the java.io.File OWASP-recommended containment defence).
|
|
525
555
|
additionalSanitizers.push(...findJavaCanonicalPathStartsWithGuardSanitizers(code));
|
|
556
|
+
// cognium-dev#293: `if (!Set.of("a", "b").contains(key)) throw` allow-list guard.
|
|
557
|
+
additionalSanitizers.push(...findJavaLiteralAllowlistGuardSanitizers(code));
|
|
558
|
+
// cognium-dev#668: `!<literal set>.contains(cls)` reject guard before reflection.
|
|
559
|
+
additionalSanitizers.push(...findJavaReflectionAllowlistGuardSanitizers(code, types));
|
|
560
|
+
// cognium-dev#566: a Spring @RestController / @ResponseBody / ResponseEntity
|
|
561
|
+
// handler's returned String is the response body. TaintPropagationPass
|
|
562
|
+
// links each synthesised sink to its source.
|
|
563
|
+
{
|
|
564
|
+
const tm = ctx.getResult('taint-matcher');
|
|
565
|
+
const xssSan = tm.sanitizers.filter(sa => sa.sanitizes.includes('xss')).map(sa => sa.line);
|
|
566
|
+
for (const r of findJavaSpringReturnXSSSinks(code, types, tm.sources, xssSan)) {
|
|
567
|
+
additionalSinks.push({
|
|
568
|
+
type: 'xss',
|
|
569
|
+
cwe: 'CWE-79',
|
|
570
|
+
line: r.sinkLine,
|
|
571
|
+
location: `Spring handler returns HTML with request input at line ${r.sinkLine}`,
|
|
572
|
+
confidence: 0.9,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
526
576
|
additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
|
|
527
577
|
// cognium-dev#293: the same strip, but living in a user-defined helper.
|
|
528
578
|
additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, 'java'));
|
|
@@ -1578,6 +1628,50 @@ function findGoRequestBodySources(sourceCode, language) {
|
|
|
1578
1628
|
}
|
|
1579
1629
|
return sources;
|
|
1580
1630
|
}
|
|
1631
|
+
/** cognium-dev#490 — split `a; b; c` on one line (no `{`), respecting string literals. */
|
|
1632
|
+
function splitCSharpDepth0Statements(line) {
|
|
1633
|
+
if (line.includes('{'))
|
|
1634
|
+
return [line];
|
|
1635
|
+
const statements = [];
|
|
1636
|
+
let start = 0;
|
|
1637
|
+
let depth = 0;
|
|
1638
|
+
let inStr = null;
|
|
1639
|
+
let esc = false;
|
|
1640
|
+
for (let i = 0; i < line.length; i++) {
|
|
1641
|
+
const c = line[i];
|
|
1642
|
+
if (inStr) {
|
|
1643
|
+
if (esc) {
|
|
1644
|
+
esc = false;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (c === '\\') {
|
|
1648
|
+
esc = true;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (c === inStr)
|
|
1652
|
+
inStr = null;
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (c === '"' || c === "'") {
|
|
1656
|
+
inStr = c;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (c === '(' || c === '[')
|
|
1660
|
+
depth++;
|
|
1661
|
+
else if (c === ')' || c === ']')
|
|
1662
|
+
depth = Math.max(0, depth - 1);
|
|
1663
|
+
else if (c === ';' && depth === 0) {
|
|
1664
|
+
const part = line.slice(start, i + 1).trim();
|
|
1665
|
+
if (part)
|
|
1666
|
+
statements.push(part);
|
|
1667
|
+
start = i + 1;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const tail = line.slice(start).trim();
|
|
1671
|
+
if (tail)
|
|
1672
|
+
statements.push(tail);
|
|
1673
|
+
return statements.length > 1 ? statements : [line];
|
|
1674
|
+
}
|
|
1581
1675
|
function findCSharpRequestSources(sourceCode, language) {
|
|
1582
1676
|
if (language !== 'csharp')
|
|
1583
1677
|
return [];
|
|
@@ -1600,6 +1694,13 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
1600
1694
|
// of defect as #350 (Java try-with-resources): a resource-declaration syntax
|
|
1601
1695
|
// the scanner never looked at.
|
|
1602
1696
|
const assignRe = /^\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
1697
|
+
// cognium-dev#638 — `arr[i] = req.QueryString["k"]` does not match `assignRe`
|
|
1698
|
+
// (LHS is an indexer). Seed the array base so cross-file calls that pass
|
|
1699
|
+
// `arr` carry taint into `string[]` / `object[]` callee parameters.
|
|
1700
|
+
const arrayElementAssignRe = /^\s*([A-Za-z_]\w*)\s*\[[^\]]+\]\s*=\s*(.+?);?\s*$/;
|
|
1701
|
+
// cognium-dev#638 step 3 — `box.value = req.QueryString["k"]` seeds the
|
|
1702
|
+
// container local so cross-file calls that pass `box` carry taint.
|
|
1703
|
+
const memberFieldAssignRe = /^\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
1603
1704
|
// `Request.*` is the inherited controller member. A parameter typed
|
|
1604
1705
|
// `HttpRequest` / `HttpRequestBase` is the same surface under whatever
|
|
1605
1706
|
// name the method uses (`req` in Juliet) — cognium-dev#501.
|
|
@@ -1729,97 +1830,136 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
1729
1830
|
// when it is not the line's code.
|
|
1730
1831
|
const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
|
|
1731
1832
|
for (let i = 0; i < lines.length; i++) {
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
// it captures `sr` with EVERYTHING after `=` as the right-hand side. That
|
|
1739
|
-
// RHS contains `sr.ReadLine()`, so the line was classified as a read and
|
|
1740
|
-
// bound to `sr` — the reader, not `data`. The embedded-assignment path
|
|
1741
|
-
// handles exactly this shape and finds both `sr` and `data`, but it only
|
|
1742
|
-
// ran when `assignRe` failed to match at all. Prefer it whenever the
|
|
1743
|
-
// captured RHS crosses a block boundary. (#504 was the mirror of this:
|
|
1744
|
-
// there `assignRe` did not match and the inner assignment was lost.)
|
|
1745
|
-
const rhsCrossesBlock = m !== null && m[2].includes('{');
|
|
1746
|
-
if (!m || rhsCrossesBlock) {
|
|
1747
|
-
const stmt = lines[i];
|
|
1748
|
-
if (stmt.includes('{') && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
1749
|
-
const emb = new RegExp(embeddedAssignRe.source, 'g');
|
|
1750
|
-
let em;
|
|
1751
|
-
let seeded = false;
|
|
1752
|
-
while ((em = emb.exec(stmt)) !== null) {
|
|
1753
|
-
const varName = em[1];
|
|
1754
|
-
const rhs = em[2];
|
|
1755
|
-
const type = classify(rhs);
|
|
1756
|
-
if (!type)
|
|
1757
|
-
continue;
|
|
1833
|
+
for (const stmt of splitCSharpDepth0Statements(lines[i])) {
|
|
1834
|
+
const arrEl = arrayElementAssignRe.exec(stmt);
|
|
1835
|
+
if (arrEl) {
|
|
1836
|
+
const [, baseName, rhs] = arrEl;
|
|
1837
|
+
const type = classify(rhs);
|
|
1838
|
+
if (type && baseName) {
|
|
1758
1839
|
const lineNumber = i + 1;
|
|
1759
|
-
if (sources.some(s => s.line === lineNumber && s.variable ===
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
seeded = true;
|
|
1840
|
+
if (!sources.some(s => s.line === lineNumber && s.variable === baseName)) {
|
|
1841
|
+
sources.push({
|
|
1842
|
+
type,
|
|
1843
|
+
location: `${baseName}[…] = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1844
|
+
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1845
|
+
line: lineNumber,
|
|
1846
|
+
confidence: 1.0,
|
|
1847
|
+
variable: baseName,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
continue;
|
|
1771
1851
|
}
|
|
1772
|
-
|
|
1852
|
+
}
|
|
1853
|
+
const memEl = memberFieldAssignRe.exec(stmt);
|
|
1854
|
+
if (memEl) {
|
|
1855
|
+
const [, baseName, fieldName, rhs] = memEl;
|
|
1856
|
+
const type = classify(rhs);
|
|
1857
|
+
if (type && baseName) {
|
|
1858
|
+
const lineNumber = i + 1;
|
|
1859
|
+
if (!sources.some(s => s.line === lineNumber && s.variable === baseName)) {
|
|
1860
|
+
sources.push({
|
|
1861
|
+
type,
|
|
1862
|
+
location: `${baseName}.${fieldName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1863
|
+
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1864
|
+
line: lineNumber,
|
|
1865
|
+
confidence: 1.0,
|
|
1866
|
+
variable: baseName,
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1773
1869
|
continue;
|
|
1870
|
+
}
|
|
1774
1871
|
}
|
|
1775
|
-
|
|
1872
|
+
const m = assignRe.exec(stmt);
|
|
1873
|
+
// cognium-dev#502 — `assignRe` is anchored and greedy, so on a one-line
|
|
1874
|
+
// block header that also contains the real assignment
|
|
1875
|
+
//
|
|
1876
|
+
// using (StreamReader sr = new StreamReader(tcp.GetStream())) { data = sr.ReadLine(); }
|
|
1877
|
+
//
|
|
1878
|
+
// it captures `sr` with EVERYTHING after `=` as the right-hand side. That
|
|
1879
|
+
// RHS contains `sr.ReadLine()`, so the line was classified as a read and
|
|
1880
|
+
// bound to `sr` — the reader, not `data`. The embedded-assignment path
|
|
1881
|
+
// handles exactly this shape and finds both `sr` and `data`, but it only
|
|
1882
|
+
// ran when `assignRe` failed to match at all. Prefer it whenever the
|
|
1883
|
+
// captured RHS crosses a block boundary. (#504 was the mirror of this:
|
|
1884
|
+
// there `assignRe` did not match and the inner assignment was lost.)
|
|
1885
|
+
const rhsCrossesBlock = m !== null && m[2].includes('{');
|
|
1886
|
+
if (!m || rhsCrossesBlock) {
|
|
1887
|
+
if (stmt.includes('{') && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
1888
|
+
const emb = new RegExp(embeddedAssignRe.source, 'g');
|
|
1889
|
+
let em;
|
|
1890
|
+
let seeded = false;
|
|
1891
|
+
while ((em = emb.exec(stmt)) !== null) {
|
|
1892
|
+
const varName = em[1];
|
|
1893
|
+
const rhs = em[2];
|
|
1894
|
+
const type = classify(rhs);
|
|
1895
|
+
if (!type)
|
|
1896
|
+
continue;
|
|
1897
|
+
const lineNumber = i + 1;
|
|
1898
|
+
if (sources.some(s => s.line === lineNumber && s.variable === varName))
|
|
1899
|
+
continue;
|
|
1900
|
+
sources.push({
|
|
1901
|
+
type,
|
|
1902
|
+
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1903
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
1904
|
+
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1905
|
+
line: lineNumber,
|
|
1906
|
+
confidence: 1.0,
|
|
1907
|
+
variable: varName,
|
|
1908
|
+
});
|
|
1909
|
+
seeded = true;
|
|
1910
|
+
}
|
|
1911
|
+
if (seeded)
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
if (!exprStmtCallRe.test(stmt) || nonCallKeywordRe.test(stmt))
|
|
1915
|
+
continue;
|
|
1916
|
+
const hit = inlineRead(stmt);
|
|
1917
|
+
if (!hit)
|
|
1918
|
+
continue;
|
|
1919
|
+
const { type, read } = hit;
|
|
1920
|
+
const lineNumber = i + 1;
|
|
1921
|
+
if (sources.some(s => s.line === lineNumber))
|
|
1922
|
+
continue;
|
|
1923
|
+
// `variable` is the `<inline>` marker (the same one the inline
|
|
1924
|
+
// colocation flow puts on its path steps), never a real name. It
|
|
1925
|
+
// must be set, and must match nothing:
|
|
1926
|
+
// - with NO variable, `findInitialTaint` seeds every def on the
|
|
1927
|
+
// FOLLOWING line by adjacency, so `Log(file.FileName);` followed by
|
|
1928
|
+
// `var x = "/tmp/a";` tainted the constant `x`;
|
|
1929
|
+
// - with the read text itself (`file.FileName`) as the variable, the
|
|
1930
|
+
// colocation path treats it as an LHS binding and hands the pair
|
|
1931
|
+
// to the variable scan, which only looks at strictly later sinks —
|
|
1932
|
+
// so the same-line flow this exists for was lost.
|
|
1933
|
+
// The marker reaches only a sink on this very statement.
|
|
1934
|
+
const code = stmt.trim();
|
|
1935
|
+
sources.push({
|
|
1936
|
+
type,
|
|
1937
|
+
location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? '...' : ''}`,
|
|
1938
|
+
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1939
|
+
line: lineNumber,
|
|
1940
|
+
confidence: 1.0,
|
|
1941
|
+
variable: '<inline>',
|
|
1942
|
+
code,
|
|
1943
|
+
});
|
|
1776
1944
|
continue;
|
|
1777
|
-
|
|
1778
|
-
|
|
1945
|
+
}
|
|
1946
|
+
const [, varName, rhs] = m;
|
|
1947
|
+
const type = classify(rhs);
|
|
1948
|
+
if (!type)
|
|
1779
1949
|
continue;
|
|
1780
|
-
const { type, read } = hit;
|
|
1781
1950
|
const lineNumber = i + 1;
|
|
1782
|
-
if (sources.some(s => s.line === lineNumber))
|
|
1783
|
-
continue;
|
|
1784
|
-
// `variable` is the `<inline>` marker (the same one the inline
|
|
1785
|
-
// colocation flow puts on its path steps), never a real name. It
|
|
1786
|
-
// must be set, and must match nothing:
|
|
1787
|
-
// - with NO variable, `findInitialTaint` seeds every def on the
|
|
1788
|
-
// FOLLOWING line by adjacency, so `Log(file.FileName);` followed by
|
|
1789
|
-
// `var x = "/tmp/a";` tainted the constant `x`;
|
|
1790
|
-
// - with the read text itself (`file.FileName`) as the variable, the
|
|
1791
|
-
// colocation path treats it as an LHS binding and hands the pair
|
|
1792
|
-
// to the variable scan, which only looks at strictly later sinks —
|
|
1793
|
-
// so the same-line flow this exists for was lost.
|
|
1794
|
-
// The marker reaches only a sink on this very statement.
|
|
1795
|
-
const code = stmt.trim();
|
|
1951
|
+
if (sources.some(s => s.line === lineNumber && s.variable === varName))
|
|
1952
|
+
continue;
|
|
1796
1953
|
sources.push({
|
|
1797
1954
|
type,
|
|
1798
|
-
location: `${
|
|
1955
|
+
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1956
|
+
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
1799
1957
|
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1800
1958
|
line: lineNumber,
|
|
1801
1959
|
confidence: 1.0,
|
|
1802
|
-
variable:
|
|
1803
|
-
code,
|
|
1960
|
+
variable: varName,
|
|
1804
1961
|
});
|
|
1805
|
-
continue;
|
|
1806
1962
|
}
|
|
1807
|
-
const [, varName, rhs] = m;
|
|
1808
|
-
const type = classify(rhs);
|
|
1809
|
-
if (!type)
|
|
1810
|
-
continue;
|
|
1811
|
-
const lineNumber = i + 1;
|
|
1812
|
-
if (sources.some(s => s.line === lineNumber && s.variable === varName))
|
|
1813
|
-
continue;
|
|
1814
|
-
sources.push({
|
|
1815
|
-
type,
|
|
1816
|
-
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1817
|
-
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
1818
|
-
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1819
|
-
line: lineNumber,
|
|
1820
|
-
confidence: 1.0,
|
|
1821
|
-
variable: varName,
|
|
1822
|
-
});
|
|
1823
1963
|
}
|
|
1824
1964
|
return sources;
|
|
1825
1965
|
}
|
|
@@ -1968,27 +2108,55 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
1968
2108
|
const patterns = isRouteHandler
|
|
1969
2109
|
? [...JS_TAINTED_PATTERNS, { pattern: /\bparams\s*[.[]/, type: 'http_path' }]
|
|
1970
2110
|
: JS_TAINTED_PATTERNS;
|
|
2111
|
+
// #667: `REGISTRY[req.query.mod]` on a constant table yields a value the
|
|
2112
|
+
// program wrote, not request data.
|
|
2113
|
+
const lookupTables = constLookupTables(sourceCode, language);
|
|
2114
|
+
// cognium-dev#582: scan every assignment on the line. A one-line
|
|
2115
|
+
// `if (c) t = "b"; else t = req.query.t` (or braced `var` arms) has the
|
|
2116
|
+
// source on a later assignment; matching only the first and truncating its
|
|
2117
|
+
// RHS dropped that source. After a control-flow header before an
|
|
2118
|
+
// assignment, that assignment owns only its own statement so a condition's
|
|
2119
|
+
// read is not bound to a body variable (`if (req.query.m) { t = "a"; } else
|
|
2120
|
+
// if (req.query.n) { … }`).
|
|
2121
|
+
// cognium-dev#490: a TypeScript declaration may carry a type annotation
|
|
2122
|
+
// (`const q: string = req.query.q`); the binding is the name before the colon,
|
|
2123
|
+
// not the annotation, so the source's own definition is found on the line.
|
|
2124
|
+
const assignmentRe = /(?:(?:var|let|const)\s+(\w+)\s*:\s*[^=;]+?\s*=(?![>=])|(?:(?:var|let|const)\s+)?(\w+)\s*=)\s*/g;
|
|
2125
|
+
const rhsOwnStmt = /^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|[^;}"'`])*/;
|
|
1971
2126
|
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
1972
2127
|
const line = lines[lineNum];
|
|
1973
2128
|
const lineNumber = lineNum + 1;
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2129
|
+
assignmentRe.lastIndex = 0;
|
|
2130
|
+
for (const assignmentMatch of line.matchAll(assignmentRe)) {
|
|
2131
|
+
const varName = assignmentMatch[1] ?? assignmentMatch[2];
|
|
2132
|
+
if (!varName)
|
|
2133
|
+
continue;
|
|
2134
|
+
let rhs = line.slice((assignmentMatch.index ?? 0) + assignmentMatch[0].length);
|
|
2135
|
+
if (/\b(?:if|while|switch|else)\b/.test(line.slice(0, assignmentMatch.index ?? 0))) {
|
|
2136
|
+
rhs = rhsOwnStmt.exec(rhs)?.[0] ?? rhs;
|
|
2137
|
+
}
|
|
2138
|
+
else if (line.includes(';', (assignmentMatch.index ?? 0) + assignmentMatch[0].length)) {
|
|
2139
|
+
// Same-line later statements: do not let a greedy RHS attribute their
|
|
2140
|
+
// reads to this variable (`a = 1; b = req.query.t`).
|
|
2141
|
+
rhs = rhsOwnStmt.exec(rhs)?.[0] ?? rhs;
|
|
2142
|
+
}
|
|
2143
|
+
if (isConstTableLookup(rhs, lookupTables))
|
|
2144
|
+
continue;
|
|
2145
|
+
for (const { pattern, type } of patterns) {
|
|
2146
|
+
if (pattern.test(rhs)) {
|
|
2147
|
+
const alreadyExists = sources.some(s => s.line === lineNumber && s.type === type && s.variable === varName);
|
|
2148
|
+
if (!alreadyExists) {
|
|
2149
|
+
sources.push({
|
|
2150
|
+
type,
|
|
2151
|
+
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
2152
|
+
severity: 'high',
|
|
2153
|
+
line: lineNumber,
|
|
2154
|
+
confidence: 1.0,
|
|
2155
|
+
variable: varName,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
break;
|
|
1990
2159
|
}
|
|
1991
|
-
break;
|
|
1992
2160
|
}
|
|
1993
2161
|
}
|
|
1994
2162
|
}
|
|
@@ -2004,6 +2172,355 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
2004
2172
|
// event-name allowlist to avoid firing on unrelated `.on('drain', cb)`
|
|
2005
2173
|
// / `.on('close', cb)` etc. that carry non-user-input.
|
|
2006
2174
|
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
2175
|
+
sources.push(...findExpressDestructuredRequestSources(sourceCode));
|
|
2176
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
2177
|
+
sources.push(...findGraphqlResolverArgSources(sourceCode));
|
|
2178
|
+
}
|
|
2179
|
+
return sources;
|
|
2180
|
+
}
|
|
2181
|
+
/** Index just past the `}` matching the `{` at `open`, or -1. */
|
|
2182
|
+
function matchBrace(text, open) {
|
|
2183
|
+
let depth = 0;
|
|
2184
|
+
for (let i = open; i < text.length; i++) {
|
|
2185
|
+
const ch = text[i];
|
|
2186
|
+
if (ch === '{')
|
|
2187
|
+
depth++;
|
|
2188
|
+
else if (ch === '}') {
|
|
2189
|
+
depth--;
|
|
2190
|
+
if (depth === 0)
|
|
2191
|
+
return i + 1;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return -1;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Index just past an optional TypeScript `: <type>` annotation starting at
|
|
2198
|
+
* `from` (`}: Request<A, B>`), i.e. at the next top-level `,` or `)`. Returns
|
|
2199
|
+
* `from` unchanged when no annotation follows.
|
|
2200
|
+
*/
|
|
2201
|
+
function skipTypeAnnotation(text, from) {
|
|
2202
|
+
const m = /^\s*:/.exec(text.slice(from));
|
|
2203
|
+
if (!m)
|
|
2204
|
+
return from;
|
|
2205
|
+
let depth = 0;
|
|
2206
|
+
for (let i = from + m[0].length; i < text.length; i++) {
|
|
2207
|
+
const ch = text[i];
|
|
2208
|
+
if (ch === '<' || ch === '(' || ch === '[' || ch === '{')
|
|
2209
|
+
depth++;
|
|
2210
|
+
else if (ch === '>' || ch === ']' || ch === '}')
|
|
2211
|
+
depth--;
|
|
2212
|
+
else if (ch === ')') {
|
|
2213
|
+
if (depth === 0)
|
|
2214
|
+
return i;
|
|
2215
|
+
depth--;
|
|
2216
|
+
}
|
|
2217
|
+
else if (ch === ',' && depth === 0)
|
|
2218
|
+
return i;
|
|
2219
|
+
else if (ch === '=' && text[i + 1] === '>' && depth === 0)
|
|
2220
|
+
return from;
|
|
2221
|
+
if (depth < 0)
|
|
2222
|
+
return from;
|
|
2223
|
+
}
|
|
2224
|
+
return from;
|
|
2225
|
+
}
|
|
2226
|
+
const EXPRESS_REQUEST_KEYS = {
|
|
2227
|
+
query: 'http_param',
|
|
2228
|
+
params: 'http_param',
|
|
2229
|
+
body: 'http_body',
|
|
2230
|
+
headers: 'http_header',
|
|
2231
|
+
cookies: 'http_cookie',
|
|
2232
|
+
};
|
|
2233
|
+
/**
|
|
2234
|
+
* cognium-dev#587 — Express handler whose first parameter destructures the
|
|
2235
|
+
* request: `app.get(path, ({ query }, res) => …)`, `({ query: q }, res)`,
|
|
2236
|
+
* `({ query: { id } }, res)`. Seeds the bound names of the allowlisted request
|
|
2237
|
+
* keys only. Gated on a receiver bound to `express()` / `express.Router()` /
|
|
2238
|
+
* `Router()` in the same file, so an arbitrary `({ query }) => …` callback is
|
|
2239
|
+
* not a source.
|
|
2240
|
+
*/
|
|
2241
|
+
function findExpressDestructuredRequestSources(sourceCode) {
|
|
2242
|
+
const sources = [];
|
|
2243
|
+
const receivers = new Set();
|
|
2244
|
+
for (const m of sourceCode.matchAll(/\b(?:const|let|var)\s+(\w+)\s*(?::\s*[\w.]+\s*)?=\s*(?:express\s*\(\s*\)|express\s*\.\s*Router\s*\(\s*\)|Router\s*\(\s*\))/g)) {
|
|
2245
|
+
receivers.add(m[1]);
|
|
2246
|
+
}
|
|
2247
|
+
if (receivers.size === 0)
|
|
2248
|
+
return sources;
|
|
2249
|
+
const REG_RE = /\b(\w+)\s*\.\s*(?:get|post|put|patch|delete|all)\s*\(\s*(['"`])[^'"`]*\2\s*,\s*(?:async\s+)?(?:function\s*\w*\s*)?\(\s*\{/g;
|
|
2250
|
+
for (const m of sourceCode.matchAll(REG_RE)) {
|
|
2251
|
+
if (!receivers.has(m[1]))
|
|
2252
|
+
continue;
|
|
2253
|
+
const open = m.index + m[0].length - 1;
|
|
2254
|
+
const close = matchBrace(sourceCode, open);
|
|
2255
|
+
if (close < 0)
|
|
2256
|
+
continue;
|
|
2257
|
+
// The pattern must be followed by another parameter or `)`, i.e. it is
|
|
2258
|
+
// the handler's first parameter.
|
|
2259
|
+
// A TypeScript `: Request` annotation after the pattern is skipped first.
|
|
2260
|
+
if (!/^\s*[,)]/.test(sourceCode.slice(skipTypeAnnotation(sourceCode, close))))
|
|
2261
|
+
continue;
|
|
2262
|
+
const line = sourceCode.slice(0, m.index + m[0].length).split('\n').length;
|
|
2263
|
+
const inner = sourceCode.slice(open + 1, close - 1);
|
|
2264
|
+
// Split top-level entries.
|
|
2265
|
+
const entries = [];
|
|
2266
|
+
let depth = 0;
|
|
2267
|
+
let start = 0;
|
|
2268
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2269
|
+
if (inner[i] === '{')
|
|
2270
|
+
depth++;
|
|
2271
|
+
else if (inner[i] === '}')
|
|
2272
|
+
depth--;
|
|
2273
|
+
else if (inner[i] === ',' && depth === 0) {
|
|
2274
|
+
entries.push(inner.slice(start, i));
|
|
2275
|
+
start = i + 1;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
entries.push(inner.slice(start));
|
|
2279
|
+
for (const raw of entries) {
|
|
2280
|
+
const entry = raw.trim();
|
|
2281
|
+
const km = entry.match(/^(\w+)\s*(?::\s*([\s\S]+))?$/);
|
|
2282
|
+
if (!km)
|
|
2283
|
+
continue;
|
|
2284
|
+
const type = EXPRESS_REQUEST_KEYS[km[1]];
|
|
2285
|
+
if (!type)
|
|
2286
|
+
continue;
|
|
2287
|
+
const target = (km[2] ?? km[1]).replace(/=[\s\S]*$/, '').trim();
|
|
2288
|
+
const names = target.startsWith('{')
|
|
2289
|
+
? [...target.slice(1, -1).matchAll(/(?:^|,)\s*(?:\w+\s*:\s*)?(\w+)/g)].map(x => x[1])
|
|
2290
|
+
: /^\w+$/.test(target) ? [target] : [];
|
|
2291
|
+
for (const name of names) {
|
|
2292
|
+
sources.push({
|
|
2293
|
+
type,
|
|
2294
|
+
location: `Express handler destructured request '${km[1]}' -> '${name}' at line ${line}`,
|
|
2295
|
+
severity: 'high',
|
|
2296
|
+
line,
|
|
2297
|
+
confidence: 0.95,
|
|
2298
|
+
variable: name,
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return sources;
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* The parameters of the call / function whose `(` is at `open`: each top-level,
|
|
2307
|
+
* comma-separated entry with its offset into `text`. A comma inside `()`, `[]`,
|
|
2308
|
+
* `{}` or a type's `<>` (the `>` of `=>` excluded) does not split. Returns null
|
|
2309
|
+
* when the list does not close.
|
|
2310
|
+
*/
|
|
2311
|
+
function readParameterList(text, open) {
|
|
2312
|
+
const params = [];
|
|
2313
|
+
let depth = 0;
|
|
2314
|
+
let start = open + 1;
|
|
2315
|
+
for (let i = open; i < text.length; i++) {
|
|
2316
|
+
const ch = text[i];
|
|
2317
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2318
|
+
for (i++; i < text.length && text[i] !== ch; i++)
|
|
2319
|
+
if (text[i] === '\\')
|
|
2320
|
+
i++;
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
if (ch === '(' || ch === '[' || ch === '{' || ch === '<')
|
|
2324
|
+
depth++;
|
|
2325
|
+
else if (ch === '>' && text[i - 1] !== '=')
|
|
2326
|
+
depth--;
|
|
2327
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
2328
|
+
depth--;
|
|
2329
|
+
if (depth === 0) {
|
|
2330
|
+
if (text.slice(start, i).trim() !== '')
|
|
2331
|
+
params.push({ text: text.slice(start, i), start });
|
|
2332
|
+
return { params };
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
else if (ch === ',' && depth === 1) {
|
|
2336
|
+
params.push({ text: text.slice(start, i), start });
|
|
2337
|
+
start = i + 1;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* The names a resolver's `args` parameter binds: the identifier itself
|
|
2344
|
+
* (`args`, `args: Args`, `args?: Args`), or every name bound by an object
|
|
2345
|
+
* pattern (`{ sku }`, `{ sku: s }`, `{ filter: { id } }`, `{ id = 1 }`,
|
|
2346
|
+
* `{ ...rest }`), each with its offset into `param`. An array pattern or a
|
|
2347
|
+
* computed key binds nothing here.
|
|
2348
|
+
*/
|
|
2349
|
+
function graphqlArgBindings(param) {
|
|
2350
|
+
const trimmed = param.replace(/^\s+/, '');
|
|
2351
|
+
const lead = param.length - trimmed.length;
|
|
2352
|
+
const ident = /^(?:\.\.\.)?([A-Za-z_$][\w$]*)\s*\??\s*(?::[\s\S]*|=[\s\S]*)?$/.exec(trimmed);
|
|
2353
|
+
if (ident)
|
|
2354
|
+
return [{ name: ident[1], offset: lead + trimmed.indexOf(ident[1]) }];
|
|
2355
|
+
if (!trimmed.startsWith('{'))
|
|
2356
|
+
return [];
|
|
2357
|
+
const close = matchBrace(trimmed, 0);
|
|
2358
|
+
if (close < 0)
|
|
2359
|
+
return [];
|
|
2360
|
+
return objectPatternBindings(trimmed, 1, close - 1).map(b => ({ name: b.name, offset: lead + b.offset }));
|
|
2361
|
+
}
|
|
2362
|
+
/** Every name a destructured (non-`args`) parameter binds, for the ambiguity check above. */
|
|
2363
|
+
function graphqlPatternNames(param) {
|
|
2364
|
+
const t = param.trim();
|
|
2365
|
+
if (!t.startsWith('{'))
|
|
2366
|
+
return [];
|
|
2367
|
+
const close = matchBrace(t, 0);
|
|
2368
|
+
if (close < 0)
|
|
2369
|
+
return [];
|
|
2370
|
+
return objectPatternBindings(t, 1, close - 1).map(b => b.name);
|
|
2371
|
+
}
|
|
2372
|
+
function objectPatternBindings(text, from, to) {
|
|
2373
|
+
const out = [];
|
|
2374
|
+
let depth = 0;
|
|
2375
|
+
let start = from;
|
|
2376
|
+
const entries = [];
|
|
2377
|
+
for (let i = from; i < to; i++) {
|
|
2378
|
+
const ch = text[i];
|
|
2379
|
+
if (ch === '{' || ch === '[' || ch === '(')
|
|
2380
|
+
depth++;
|
|
2381
|
+
else if (ch === '}' || ch === ']' || ch === ')')
|
|
2382
|
+
depth--;
|
|
2383
|
+
else if (ch === ',' && depth === 0) {
|
|
2384
|
+
entries.push([start, i]);
|
|
2385
|
+
start = i + 1;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
entries.push([start, to]);
|
|
2389
|
+
for (const [a, b] of entries) {
|
|
2390
|
+
const raw = text.slice(a, b);
|
|
2391
|
+
const body = raw.trimStart();
|
|
2392
|
+
const at = a + (raw.length - body.length);
|
|
2393
|
+
if (body.trim() === '')
|
|
2394
|
+
continue;
|
|
2395
|
+
const rest = /^\.\.\.\s*([A-Za-z_$][\w$]*)\s*$/.exec(body.trimEnd());
|
|
2396
|
+
if (rest) {
|
|
2397
|
+
out.push({ name: rest[1], offset: at + body.indexOf(rest[1]) });
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const keyed = /^(?:[A-Za-z_$][\w$]*|'[^']*'|"[^"]*")\s*:\s*/.exec(body);
|
|
2401
|
+
if (keyed) {
|
|
2402
|
+
const targetAt = at + keyed[0].length;
|
|
2403
|
+
const target = body.slice(keyed[0].length);
|
|
2404
|
+
if (target.startsWith('{')) {
|
|
2405
|
+
const end = matchBrace(text, targetAt);
|
|
2406
|
+
if (end > 0)
|
|
2407
|
+
out.push(...objectPatternBindings(text, targetAt + 1, end - 1));
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const id = /^([A-Za-z_$][\w$]*)\s*(?:=[\s\S]*)?$/.exec(target.trimEnd());
|
|
2411
|
+
if (id)
|
|
2412
|
+
out.push({ name: id[1], offset: targetAt });
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
const short = /^([A-Za-z_$][\w$]*)\s*(?:=[\s\S]*)?$/.exec(body.trimEnd());
|
|
2416
|
+
if (short)
|
|
2417
|
+
out.push({ name: short[1], offset: at });
|
|
2418
|
+
}
|
|
2419
|
+
return out;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* cognium-dev#586 — GraphQL resolver-map arguments. The second parameter of a
|
|
2423
|
+
* function under `Query` / `Mutation` / `Subscription` in the object passed as
|
|
2424
|
+
* `resolvers` to `new ApolloServer({…})` / `makeExecutableSchema({…})` (inline,
|
|
2425
|
+
* or a same-file `const resolvers = {…}`, optionally type-annotated) is client-supplied. Parameter 0
|
|
2426
|
+
* (`parent`) and 2 (`context`) are never seeded.
|
|
2427
|
+
*/
|
|
2428
|
+
function findGraphqlResolverArgSources(sourceCode) {
|
|
2429
|
+
const sources = [];
|
|
2430
|
+
const ctor = /(?:new\s+ApolloServer|makeExecutableSchema)\s*\(\s*\{/g;
|
|
2431
|
+
const maps = [];
|
|
2432
|
+
for (const m of sourceCode.matchAll(ctor)) {
|
|
2433
|
+
const open = m.index + m[0].length - 1;
|
|
2434
|
+
const close = matchBrace(sourceCode, open);
|
|
2435
|
+
if (close < 0)
|
|
2436
|
+
continue;
|
|
2437
|
+
const opts = sourceCode.slice(open, close);
|
|
2438
|
+
const inline = opts.match(/\bresolvers\s*:\s*\{/);
|
|
2439
|
+
if (inline) {
|
|
2440
|
+
maps.push(open + inline.index + inline[0].length - 1);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
const named = opts.match(/\bresolvers\s*(?::\s*(\w+))?\s*[,}]/);
|
|
2444
|
+
if (!named)
|
|
2445
|
+
continue;
|
|
2446
|
+
const ident = named[1] ?? 'resolvers';
|
|
2447
|
+
// cognium-dev#752: an annotation between the name and `=` (`: Resolvers`,
|
|
2448
|
+
// `: Resolvers<Context>`, `: gql.Resolvers<Ctx, Parent>`) does not hide the map.
|
|
2449
|
+
const decl = new RegExp(`\\b(?:const|let|var)\\s+${ident}\\s*(?::\\s*[A-Za-z_$][\\w$]*(?:\\s*\\.\\s*[A-Za-z_$][\\w$]*)*` +
|
|
2450
|
+
`(?:\\s*<[^<>=]*(?:<[^<>=]*>[^<>=]*)*>)?\\s*)?=\\s*\\{`).exec(sourceCode);
|
|
2451
|
+
if (decl)
|
|
2452
|
+
maps.push(decl.index + decl[0].length - 1);
|
|
2453
|
+
}
|
|
2454
|
+
// `name: (…) =>`, `name: async (…) =>`, `name: function (…)`, `name(…) {` and `async name(…)`.
|
|
2455
|
+
// The parameter list is read whole, so a type annotation or a destructured
|
|
2456
|
+
// pattern on any parameter does not hide the second one.
|
|
2457
|
+
const FN_RE = /(?:^|[\s,{])(\w+)\s*(?::\s*(?:async\s+)?(?:function\s*\w*\s*)?\(|\()/g;
|
|
2458
|
+
for (const open of maps) {
|
|
2459
|
+
const close = matchBrace(sourceCode, open);
|
|
2460
|
+
if (close < 0)
|
|
2461
|
+
continue;
|
|
2462
|
+
const body = sourceCode.slice(open, close);
|
|
2463
|
+
for (const t of body.matchAll(/\b(Query|Mutation|Subscription)\s*:\s*\{/g)) {
|
|
2464
|
+
const tOpen = open + t.index + t[0].length - 1;
|
|
2465
|
+
const tClose = matchBrace(sourceCode, tOpen);
|
|
2466
|
+
if (tClose < 0)
|
|
2467
|
+
continue;
|
|
2468
|
+
// Only direct members of the type object (depth 1).
|
|
2469
|
+
const typeBody = sourceCode.slice(tOpen + 1, tClose - 1);
|
|
2470
|
+
let depth = 0;
|
|
2471
|
+
const depthAt = [];
|
|
2472
|
+
for (let i = 0; i < typeBody.length; i++) {
|
|
2473
|
+
depthAt.push(depth);
|
|
2474
|
+
const ch = typeBody[i];
|
|
2475
|
+
if (ch === '{' || ch === '(')
|
|
2476
|
+
depth++;
|
|
2477
|
+
else if (ch === '}' || ch === ')')
|
|
2478
|
+
depth--;
|
|
2479
|
+
}
|
|
2480
|
+
const found = [];
|
|
2481
|
+
// Names the resolvers in this type object bind from `parent` or `context`.
|
|
2482
|
+
// A name shared with an `args` binding is ambiguous across functions, so
|
|
2483
|
+
// it is not seeded.
|
|
2484
|
+
const otherNames = new Set();
|
|
2485
|
+
for (const f of typeBody.matchAll(FN_RE)) {
|
|
2486
|
+
const at = f.index + f[0].indexOf(f[1]);
|
|
2487
|
+
if (depthAt[at] !== 0)
|
|
2488
|
+
continue;
|
|
2489
|
+
const parenAt = f.index + f[0].length - 1;
|
|
2490
|
+
const list = readParameterList(typeBody, parenAt);
|
|
2491
|
+
if (!list)
|
|
2492
|
+
continue;
|
|
2493
|
+
list.params.forEach((param, idx) => {
|
|
2494
|
+
if (idx === 1)
|
|
2495
|
+
return;
|
|
2496
|
+
for (const b of graphqlPatternNames(param.text))
|
|
2497
|
+
otherNames.add(b);
|
|
2498
|
+
});
|
|
2499
|
+
// Only the second parameter (`args`) is client-supplied. `parent` and
|
|
2500
|
+
// `context` are never seeded, destructured or not.
|
|
2501
|
+
const second = list.params[1];
|
|
2502
|
+
if (!second)
|
|
2503
|
+
continue;
|
|
2504
|
+
const absStart = tOpen + 1 + second.start;
|
|
2505
|
+
for (const b of graphqlArgBindings(second.text)) {
|
|
2506
|
+
const line = sourceCode.slice(0, absStart + b.offset).split('\n').length;
|
|
2507
|
+
found.push({ fn: f[1], b: { name: b.name, line } });
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
for (const { fn, b } of found) {
|
|
2511
|
+
if (otherNames.has(b.name))
|
|
2512
|
+
continue;
|
|
2513
|
+
sources.push({
|
|
2514
|
+
type: 'http_param',
|
|
2515
|
+
location: `GraphQL ${t[1]}.${fn} resolver args '${b.name}' at line ${b.line}`,
|
|
2516
|
+
severity: 'high',
|
|
2517
|
+
line: b.line,
|
|
2518
|
+
confidence: 0.9,
|
|
2519
|
+
variable: b.name,
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2007
2524
|
return sources;
|
|
2008
2525
|
}
|
|
2009
2526
|
/**
|
|
@@ -2627,6 +3144,480 @@ function isSafeJinjaReturnExpr(expr) {
|
|
|
2627
3144
|
const trimmed = expr.trim();
|
|
2628
3145
|
return JINJA_SAFE_RTS_RE.test(trimmed) || JINJA_SAFE_TEMPLATE_RE.test(trimmed);
|
|
2629
3146
|
}
|
|
3147
|
+
/**
|
|
3148
|
+
* The statements on one Java source line, split at each `;` that is outside a
|
|
3149
|
+
* string / char literal and outside a comment. Comment text (`//` to the end of
|
|
3150
|
+
* the line, inline or unterminated `/* ... *\/`) is dropped; literals are kept.
|
|
3151
|
+
* `terminated` is false for a trailing piece with no `;` on this line.
|
|
3152
|
+
*/
|
|
3153
|
+
function javaLineStatements(line) {
|
|
3154
|
+
const out = [];
|
|
3155
|
+
let cur = '';
|
|
3156
|
+
let inStr = null;
|
|
3157
|
+
let escaped = false;
|
|
3158
|
+
for (let i = 0; i < line.length; i++) {
|
|
3159
|
+
const ch = line[i];
|
|
3160
|
+
if (inStr) {
|
|
3161
|
+
cur += ch;
|
|
3162
|
+
if (escaped) {
|
|
3163
|
+
escaped = false;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
if (ch === '\\') {
|
|
3167
|
+
escaped = true;
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
if (ch === inStr)
|
|
3171
|
+
inStr = null;
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
3174
|
+
if (ch === '"' || ch === "'") {
|
|
3175
|
+
inStr = ch;
|
|
3176
|
+
cur += ch;
|
|
3177
|
+
continue;
|
|
3178
|
+
}
|
|
3179
|
+
if (ch === '/' && line[i + 1] === '/')
|
|
3180
|
+
break;
|
|
3181
|
+
if (ch === '/' && line[i + 1] === '*') {
|
|
3182
|
+
const end = line.indexOf('*/', i + 2);
|
|
3183
|
+
if (end < 0)
|
|
3184
|
+
break;
|
|
3185
|
+
cur += ' ';
|
|
3186
|
+
i = end + 1;
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
if (ch === ';') {
|
|
3190
|
+
out.push({ text: cur, terminated: true });
|
|
3191
|
+
cur = '';
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
cur += ch;
|
|
3195
|
+
}
|
|
3196
|
+
if (cur.trim())
|
|
3197
|
+
out.push({ text: cur, terminated: false });
|
|
3198
|
+
return out;
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* `[modifiers] [Type] name = rhs` as ONE statement (no `;`; see
|
|
3202
|
+
* `javaLineStatements`). `==` is not an assignment; `+=` and field targets
|
|
3203
|
+
* (`a.b = x`) are not matched. Group 1 is the name, group 2 the right-hand side.
|
|
3204
|
+
*/
|
|
3205
|
+
const JAVA_STMT_ASSIGN = /^\s*(?:(?:final|public|private|protected|static|volatile|transient)\s+)*(?:[\p{L}_][\p{L}\p{N}_.]*(?:\s*<[^=]*>)?(?:\s*\[\s*\])*\s+)?([\p{L}_][\p{L}\p{N}_]*)\s*=(?!=)\s*([\s\S]*)$/u;
|
|
3206
|
+
/**
|
|
3207
|
+
* Arguments of `receiver.append(...)` on one statement, including a chain of
|
|
3208
|
+
* further `.append(...)` calls on the same expression. Stops at any other
|
|
3209
|
+
* method (e.g. `.indexOf(...)`) or at the end of the statement. Nested calls
|
|
3210
|
+
* inside an argument (e.g. `append(escape(who))`) stay inside that argument.
|
|
3211
|
+
* String / char literal bodies are walked so a `)` inside a string does not
|
|
3212
|
+
* close the argument.
|
|
3213
|
+
*/
|
|
3214
|
+
function javaAppendArgExprs(line) {
|
|
3215
|
+
const start = /^\s*([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*append\s*\(/u.exec(line);
|
|
3216
|
+
if (!start)
|
|
3217
|
+
return null;
|
|
3218
|
+
const receiver = start[1];
|
|
3219
|
+
const args = [];
|
|
3220
|
+
let i = start[0].length;
|
|
3221
|
+
const takeArg = () => {
|
|
3222
|
+
let depth = 1;
|
|
3223
|
+
let inStr = null;
|
|
3224
|
+
let escaped = false;
|
|
3225
|
+
const from = i;
|
|
3226
|
+
for (; i < line.length; i++) {
|
|
3227
|
+
const ch = line[i];
|
|
3228
|
+
if (inStr) {
|
|
3229
|
+
if (escaped) {
|
|
3230
|
+
escaped = false;
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
if (ch === '\\') {
|
|
3234
|
+
escaped = true;
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
if (ch === inStr)
|
|
3238
|
+
inStr = null;
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
if (ch === '"' || ch === "'") {
|
|
3242
|
+
inStr = ch;
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
if (ch === '(') {
|
|
3246
|
+
depth++;
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (ch === ')') {
|
|
3250
|
+
depth--;
|
|
3251
|
+
if (depth === 0) {
|
|
3252
|
+
const arg = line.slice(from, i);
|
|
3253
|
+
i++; // past ')'
|
|
3254
|
+
return arg;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
return null;
|
|
3259
|
+
};
|
|
3260
|
+
while (true) {
|
|
3261
|
+
const arg = takeArg();
|
|
3262
|
+
if (arg === null)
|
|
3263
|
+
return null;
|
|
3264
|
+
args.push(arg);
|
|
3265
|
+
// More chained .append(…)? Otherwise stop (other method, ';', end).
|
|
3266
|
+
const rest = line.slice(i);
|
|
3267
|
+
const next = /^\s*\.\s*append\s*\(/.exec(rest);
|
|
3268
|
+
if (!next)
|
|
3269
|
+
break;
|
|
3270
|
+
i += next[0].length;
|
|
3271
|
+
}
|
|
3272
|
+
return { receiver, args };
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* The source call named by a source location (`req.getParameter() in execute`,
|
|
3276
|
+
* `getParameter() in execute`) as a regex over `codeOnly` text: the receiver
|
|
3277
|
+
* chain must match too (any spacing between its tokens), and a call with no
|
|
3278
|
+
* receiver must not follow a `.`. `byName` is the method name alone, for a
|
|
3279
|
+
* receiver whose text is not on the line as written. `method` / `receiver`
|
|
3280
|
+
* are the unescaped names from the location. Null when the location names no
|
|
3281
|
+
* call.
|
|
3282
|
+
*/
|
|
3283
|
+
function javaSourceCallRe(location) {
|
|
3284
|
+
const callee = /^([\s\S]*?)\(\)(?:\s+in\s+[^()]*)?$/u.exec((location ?? '').trim())?.[1]?.trim();
|
|
3285
|
+
if (!callee)
|
|
3286
|
+
return null;
|
|
3287
|
+
const cm = /^(?:([\s\S]*?)\s*\.\s*)?([\p{L}_$][\p{L}\p{N}_$]*)$/u.exec(callee);
|
|
3288
|
+
if (!cm)
|
|
3289
|
+
return null;
|
|
3290
|
+
const esc = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3291
|
+
const methodName = cm[2];
|
|
3292
|
+
const method = esc(methodName);
|
|
3293
|
+
const byName = new RegExp(`(?<![\\p{L}\\p{N}_$])${method}\\s*\\(`, 'u');
|
|
3294
|
+
if (!cm[1]) {
|
|
3295
|
+
return {
|
|
3296
|
+
exact: new RegExp(`(?<![\\p{L}\\p{N}_$.])(?<!\\.\\s+)${method}\\s*\\(`, 'u'),
|
|
3297
|
+
byName, method: methodName, receiver: null,
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
// Receiver text as written in the location, with literal bodies blanked like the
|
|
3301
|
+
// statement text it is matched against.
|
|
3302
|
+
const recv = cm[1]
|
|
3303
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, ' ')
|
|
3304
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ');
|
|
3305
|
+
const toks = recv.match(/[\p{L}\p{N}_$]+|\S/gu);
|
|
3306
|
+
if (!toks)
|
|
3307
|
+
return null;
|
|
3308
|
+
return {
|
|
3309
|
+
exact: new RegExp(`(?<![\\p{L}\\p{N}_$.])(?<!\\.\\s+)${toks.map(esc).join('\\s*')}\\s*\\.\\s*${method}\\s*\\(`, 'u'),
|
|
3310
|
+
byName, method: methodName, receiver: toks.join(''),
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
/** Keywords that must not be treated as a Java type name in a declaration. */
|
|
3314
|
+
const JAVA_NON_TYPE_NAMES = new Set([
|
|
3315
|
+
'return', 'if', 'else', 'while', 'for', 'switch', 'case', 'new', 'throw', 'try',
|
|
3316
|
+
'catch', 'class', 'interface', 'package', 'import', 'assert', 'synchronized',
|
|
3317
|
+
'do', 'break', 'continue', 'finally', 'default', 'this', 'super', 'true',
|
|
3318
|
+
'false', 'null', 'instanceof', 'extends', 'implements', 'throws', 'enum',
|
|
3319
|
+
]);
|
|
3320
|
+
/**
|
|
3321
|
+
* Simple type name → last segment of an FQN, generics stripped
|
|
3322
|
+
* (`javax.servlet.http.HttpServletRequest` → `HttpServletRequest`).
|
|
3323
|
+
*/
|
|
3324
|
+
function javaSimpleTypeName(type) {
|
|
3325
|
+
return type.replace(/\s+/g, '').replace(/<.*$/, '').replace(/^.*\./, '');
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Locals and parameters declared in `[startLine, endLine]` (1-based), mapped to
|
|
3329
|
+
* their simple type name. Used so a line that lost a second request-source to
|
|
3330
|
+
* per-line dedup can still seed every receiver of that source class.
|
|
3331
|
+
*/
|
|
3332
|
+
function javaMethodVarTypes(lines, startLine, endLine) {
|
|
3333
|
+
const out = new Map();
|
|
3334
|
+
// Optional modifiers, then Type (FQN / simple, optional generics), then name,
|
|
3335
|
+
// then `=` `,` `;` or `)` (parameter list). Strings / comments blanked first.
|
|
3336
|
+
const decl = /(?:(?:public|private|protected|static|final|volatile|transient)\s+)*(?<![\p{L}\p{N}_$.])((?:[\p{L}_$][\p{L}\p{N}_$]*\s*\.\s*)*[\p{L}_$][\p{L}\p{N}_$]*)\s*(?:<[^;{}()=]*>)?\s+([\p{L}_$][\p{L}\p{N}_$]*)\s*(?:=|,|;|\))/gu;
|
|
3337
|
+
for (let i = startLine - 1; i < endLine && i < lines.length; i++) {
|
|
3338
|
+
const text = (lines[i] ?? '')
|
|
3339
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, ' ')
|
|
3340
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ')
|
|
3341
|
+
.replace(/\/\*.*?\*\//g, ' ')
|
|
3342
|
+
.replace(/\/\/.*$/, '');
|
|
3343
|
+
decl.lastIndex = 0;
|
|
3344
|
+
for (let m = decl.exec(text); m; m = decl.exec(text)) {
|
|
3345
|
+
const simple = javaSimpleTypeName(m[1]);
|
|
3346
|
+
if (!simple || JAVA_NON_TYPE_NAMES.has(simple))
|
|
3347
|
+
continue;
|
|
3348
|
+
out.set(m[2], simple);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return out;
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Call matcher for a source location that accepts every receiver declared with
|
|
3355
|
+
* the same simple type as the location's stored receiver (not only that
|
|
3356
|
+
* receiver). Falls back to `exact` when the stored receiver's type is unknown.
|
|
3357
|
+
* When the location names no receiver, returns `exact` (method-only form).
|
|
3358
|
+
*/
|
|
3359
|
+
function javaSameClassCallRe(srcRes, varTypes) {
|
|
3360
|
+
if (!srcRes.receiver)
|
|
3361
|
+
return srcRes.exact;
|
|
3362
|
+
const esc = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3363
|
+
// Location receiver may be a chain (`this.req`); the declared name is the
|
|
3364
|
+
// last identifier.
|
|
3365
|
+
const recvId = /[\p{L}_$][\p{L}\p{N}_$]*$/u.exec(srcRes.receiver)?.[0] ?? srcRes.receiver;
|
|
3366
|
+
const names = new Set([recvId]);
|
|
3367
|
+
const ty = varTypes.get(recvId);
|
|
3368
|
+
if (ty) {
|
|
3369
|
+
for (const [n, t] of varTypes)
|
|
3370
|
+
if (t === ty)
|
|
3371
|
+
names.add(n);
|
|
3372
|
+
}
|
|
3373
|
+
const alts = [...names].map(esc).join('|');
|
|
3374
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_$.])(?<!\\.\\s+)(?:${alts})\\s*\\.\\s*${esc(srcRes.method)}\\s*\\(`, 'u');
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* `expr` (already `codeOnly`) with every call to the named method blanked out,
|
|
3378
|
+
* from its name through its closing `)` (or the end of `expr` when the call
|
|
3379
|
+
* does not close there). `found` is true when at least one call was blanked.
|
|
3380
|
+
*/
|
|
3381
|
+
function blankJavaCalls(expr, method) {
|
|
3382
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_$])${method.replace(/[$]/g, '\\$&')}\\s*\\(`, 'gu');
|
|
3383
|
+
let text = expr;
|
|
3384
|
+
let found = false;
|
|
3385
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
3386
|
+
let depth = 1;
|
|
3387
|
+
let k = m.index + m[0].length;
|
|
3388
|
+
for (; k < text.length && depth > 0; k++) {
|
|
3389
|
+
if (text[k] === '(')
|
|
3390
|
+
depth++;
|
|
3391
|
+
else if (text[k] === ')')
|
|
3392
|
+
depth--;
|
|
3393
|
+
}
|
|
3394
|
+
text = `${text.slice(0, m.index)} 0 ${text.slice(k)}`;
|
|
3395
|
+
re.lastIndex = m.index + 3;
|
|
3396
|
+
found = true;
|
|
3397
|
+
}
|
|
3398
|
+
return { text, found };
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* cognium-dev#624 (minimal slice) — JSPWiki plugin output is rendered as HTML.
|
|
3402
|
+
*
|
|
3403
|
+
* The `String` returned from `execute(...)` of a class that implements
|
|
3404
|
+
* `org.apache.wiki.api.plugin.WikiPlugin` (or the older `...plugin.Plugin`) is
|
|
3405
|
+
* written into the page unescaped, so a request- or markup-derived value that
|
|
3406
|
+
* reaches the return is reflected XSS. The rule keys on the interface (from the
|
|
3407
|
+
* import or a fully-qualified `implements`), never on the method name: another
|
|
3408
|
+
* class's `execute(...)` is untouched.
|
|
3409
|
+
*
|
|
3410
|
+
* Mirrors `findPythonReturnXSSSinks` (#368): one sink per tainted `return`, and
|
|
3411
|
+
* `TaintPropagationPass` links it to a source. `StringBuilder.append` is
|
|
3412
|
+
* deliberately not an xss sink on its own (#268) — a builder only matters here
|
|
3413
|
+
* when it reaches the return. An xss-covering sanitizer between the source and
|
|
3414
|
+
* the return suppresses the site.
|
|
3415
|
+
*/
|
|
3416
|
+
export function findJavaWikiPluginReturnXSSSinks(code, types, imports, sources, xssSanitizers) {
|
|
3417
|
+
if (sources.length === 0)
|
|
3418
|
+
return [];
|
|
3419
|
+
const PKG = 'org.apache.wiki.api.plugin';
|
|
3420
|
+
// Each simple name counts only when that name was imported (a wildcard of the
|
|
3421
|
+
// plugin package accepts both): importing `WikiPlugin` does not make an
|
|
3422
|
+
// `implements Plugin` (some other interface) a wiki plugin, nor the reverse.
|
|
3423
|
+
const importedSimple = new Set();
|
|
3424
|
+
let wildcardImported = false;
|
|
3425
|
+
for (const i of imports) {
|
|
3426
|
+
if (i.from_package !== PKG)
|
|
3427
|
+
continue;
|
|
3428
|
+
if (i.is_wildcard)
|
|
3429
|
+
wildcardImported = true;
|
|
3430
|
+
else if (i.imported_name === 'WikiPlugin' || i.imported_name === 'Plugin')
|
|
3431
|
+
importedSimple.add(i.imported_name);
|
|
3432
|
+
}
|
|
3433
|
+
const isPluginIface = (name) => {
|
|
3434
|
+
const bare = name.replace(/<.*$/, '').trim();
|
|
3435
|
+
if (bare === `${PKG}.WikiPlugin` || bare === `${PKG}.Plugin`)
|
|
3436
|
+
return true;
|
|
3437
|
+
return (bare === 'WikiPlugin' || bare === 'Plugin') && (wildcardImported || importedSimple.has(bare));
|
|
3438
|
+
};
|
|
3439
|
+
const lines = code.split('\n');
|
|
3440
|
+
// Code positions only: string / char literal bodies blanked, then inline `/* */` and
|
|
3441
|
+
// trailing `//` comments dropped (after the literals, so a `"http://…"` is not cut).
|
|
3442
|
+
const codeOnly = (expr) => expr
|
|
3443
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, ' ')
|
|
3444
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ')
|
|
3445
|
+
.replace(/\/\*.*?\*\//g, ' ')
|
|
3446
|
+
.replace(/\/\/.*$/, '');
|
|
3447
|
+
const refs = (expr, name) => new RegExp(`(?<![\\p{L}\\p{N}_.])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'u').test(expr);
|
|
3448
|
+
const out = [];
|
|
3449
|
+
for (const t of types) {
|
|
3450
|
+
if (!t.implements.some(isPluginIface))
|
|
3451
|
+
continue;
|
|
3452
|
+
for (const m of t.methods) {
|
|
3453
|
+
if (m.name !== 'execute' || m.return_type !== 'String')
|
|
3454
|
+
continue;
|
|
3455
|
+
const inMethod = sources.filter(sc => sc.line >= m.start_line && sc.line <= m.end_line);
|
|
3456
|
+
if (inMethod.length === 0)
|
|
3457
|
+
continue;
|
|
3458
|
+
// Sources that name a variable (the carriers are walked per return below).
|
|
3459
|
+
const perSource = inMethod
|
|
3460
|
+
.filter((sc) => !!sc.variable)
|
|
3461
|
+
.map(sc => ({ sc }));
|
|
3462
|
+
if (perSource.length === 0)
|
|
3463
|
+
continue;
|
|
3464
|
+
for (let i = m.start_line - 1; i < m.end_line; i++) {
|
|
3465
|
+
const rm = /^\s*return\s+(.+?);\s*$/.exec(lines[i] ?? '');
|
|
3466
|
+
if (!rm)
|
|
3467
|
+
continue;
|
|
3468
|
+
const sinkLine = i + 1;
|
|
3469
|
+
let best = null;
|
|
3470
|
+
for (const { sc } of perSource) {
|
|
3471
|
+
if (sc.line > sinkLine)
|
|
3472
|
+
continue;
|
|
3473
|
+
// Carriers: the names the source's value reaches before the return, with
|
|
3474
|
+
// the line each became tainted. Walked one statement at a time, in order,
|
|
3475
|
+
// from the source's own statement: a name is live only after the statement
|
|
3476
|
+
// that gives it a tainted value (the source call, or an already-live
|
|
3477
|
+
// carrier), so a statement earlier on the same line sees nothing. Only
|
|
3478
|
+
// names inside a statement's append(...) arguments (chained .append and
|
|
3479
|
+
// nested calls included) taint its receiver, only its `+=` right-hand
|
|
3480
|
+
// side taints its target, and only its `=` right-hand side taints the
|
|
3481
|
+
// assigned name. A name in another method or statement does not.
|
|
3482
|
+
const carriers = new Map();
|
|
3483
|
+
const refsCarrier = (expr) => {
|
|
3484
|
+
const c = codeOnly(expr);
|
|
3485
|
+
return [...carriers.keys()].some(v => refs(c, v));
|
|
3486
|
+
};
|
|
3487
|
+
// Where the source enters on its line. Call sources are deduped to one
|
|
3488
|
+
// per line, so the surviving location names only one receiver
|
|
3489
|
+
// ("req.getParameter() in execute"). Accept every call whose receiver
|
|
3490
|
+
// shares that source class (another HttpServletRequest on the line),
|
|
3491
|
+
// not only the stored receiver — otherwise a second request call never
|
|
3492
|
+
// seeds its value. `prefs.getParameter` stays out: different class.
|
|
3493
|
+
// `byName` (method alone) must not run when the line has another
|
|
3494
|
+
// same-named call: that reintroduces the method-name FP. A parameter /
|
|
3495
|
+
// annotation source (no call) is live from its own assignment on the
|
|
3496
|
+
// line, or from the start of the line.
|
|
3497
|
+
const srcStmts = javaLineStatements(lines[sc.line - 1] ?? '');
|
|
3498
|
+
const srcRes = javaSourceCallRe(sc.location);
|
|
3499
|
+
const varTypes = javaMethodVarTypes(lines, m.start_line, m.end_line);
|
|
3500
|
+
let callRe = null;
|
|
3501
|
+
if (srcRes) {
|
|
3502
|
+
const hasExact = srcStmts.some(st => srcRes.exact.test(codeOnly(st.text)));
|
|
3503
|
+
const byNameHits = srcStmts.filter(st => srcRes.byName.test(codeOnly(st.text))).length;
|
|
3504
|
+
if (hasExact)
|
|
3505
|
+
callRe = javaSameClassCallRe(srcRes, varTypes);
|
|
3506
|
+
else if (byNameHits === 1)
|
|
3507
|
+
callRe = srcRes.byName;
|
|
3508
|
+
else
|
|
3509
|
+
callRe = srcRes.receiver ? javaSameClassCallRe(srcRes, varTypes) : null;
|
|
3510
|
+
}
|
|
3511
|
+
const callsSource = (expr) => !!callRe && callRe.test(codeOnly(expr));
|
|
3512
|
+
const seeded = srcStmts.some(st => callsSource(st.text));
|
|
3513
|
+
// xss sanitizer calls on the source line: a value that reaches a
|
|
3514
|
+
// statement only inside one of them is not tainted by it. A sanitizer
|
|
3515
|
+
// that cannot be found in the line's text covers the whole line.
|
|
3516
|
+
const srcLineSan = sc.line < sinkLine
|
|
3517
|
+
? xssSanitizers.filter(sa => sa.line === sc.line).map(sa => ({
|
|
3518
|
+
method: /([\p{L}_$][\p{L}\p{N}_$]*)\s*(?:\(\))?\s*$/u.exec(sa.method ?? '')?.[1],
|
|
3519
|
+
}))
|
|
3520
|
+
: [];
|
|
3521
|
+
const sanMethods = srcLineSan.map(sa => sa.method).filter((x) => !!x);
|
|
3522
|
+
const srcLineSanFound = srcLineSan.length > 0 && srcLineSan.every(sa => !!sa.method && srcStmts.some(st => blankJavaCalls(codeOnly(st.text), sa.method).found));
|
|
3523
|
+
const unsanitized = (expr) => {
|
|
3524
|
+
let text = codeOnly(expr);
|
|
3525
|
+
if (srcLineSanFound)
|
|
3526
|
+
for (const sm of sanMethods)
|
|
3527
|
+
text = blankJavaCalls(text, sm).text;
|
|
3528
|
+
return text;
|
|
3529
|
+
};
|
|
3530
|
+
// Unseeded: index of the statement after which `sc.variable` is live.
|
|
3531
|
+
let liveAfter = -1;
|
|
3532
|
+
if (!seeded)
|
|
3533
|
+
liveAfter = srcStmts.findIndex(st => JAVA_STMT_ASSIGN.exec(st.text)?.[1] === sc.variable);
|
|
3534
|
+
// A source on the return line itself is checked against the return as is.
|
|
3535
|
+
if ((!seeded && liveAfter < 0) || sc.line === sinkLine)
|
|
3536
|
+
carriers.set(sc.variable, sc.line);
|
|
3537
|
+
for (let j = sc.line - 1; j < i; j++) {
|
|
3538
|
+
const onSrcLine = j === sc.line - 1;
|
|
3539
|
+
const stmts = onSrcLine ? srcStmts : javaLineStatements(lines[j] ?? '');
|
|
3540
|
+
for (let k = 0; k < stmts.length; k++) {
|
|
3541
|
+
const st = stmts[k];
|
|
3542
|
+
const raw = (expr) => refsCarrier(expr) || (onSrcLine && seeded && callsSource(expr));
|
|
3543
|
+
const tainted = (expr) => (onSrcLine ? raw(unsanitized(expr)) : raw(expr));
|
|
3544
|
+
const as = JAVA_STMT_ASSIGN.exec(st.text);
|
|
3545
|
+
// A name reassigned on the source line from a sanitized value only
|
|
3546
|
+
// (`who = escapeHtml4(who)`) no longer carries the source.
|
|
3547
|
+
if (as && onSrcLine && srcLineSanFound && carriers.has(as[1]) && raw(as[2]) && !tainted(as[2]))
|
|
3548
|
+
carriers.delete(as[1]);
|
|
3549
|
+
else if (as && !carriers.has(as[1]) && tainted(as[2]))
|
|
3550
|
+
carriers.set(as[1], j + 1);
|
|
3551
|
+
const ap = javaAppendArgExprs(st.text);
|
|
3552
|
+
if (ap && !carriers.has(ap.receiver) && ap.args.some(a => tainted(a)))
|
|
3553
|
+
carriers.set(ap.receiver, j + 1);
|
|
3554
|
+
const cm = st.terminated ? /^\s*([\p{L}_][\p{L}\p{N}_]*)\s*\+=\s*(.+?)\s*$/u.exec(st.text) : null;
|
|
3555
|
+
if (cm && !carriers.has(cm[1]) && tainted(cm[2]))
|
|
3556
|
+
carriers.set(cm[1], j + 1);
|
|
3557
|
+
if (onSrcLine && !seeded && k === liveAfter && !carriers.has(sc.variable))
|
|
3558
|
+
carriers.set(sc.variable, sc.line);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
if (!refsCarrier(rm[1]))
|
|
3562
|
+
continue;
|
|
3563
|
+
// An xss-covering sanitizer only counts for this source when it is on a
|
|
3564
|
+
// later line that names one of its carriers. One on the source's own
|
|
3565
|
+
// line already took effect in the walk above (only the value inside
|
|
3566
|
+
// its argument is clean); one there that is not found in the line's
|
|
3567
|
+
// text, or a source on the return line, still covers the line.
|
|
3568
|
+
const sanitized = xssSanitizers.some(({ line: l }) => l >= sc.line && l <= sinkLine &&
|
|
3569
|
+
(l === sc.line ? !srcLineSanFound : refsCarrier(lines[l - 1] ?? '')));
|
|
3570
|
+
if (sanitized)
|
|
3571
|
+
continue;
|
|
3572
|
+
if (!best || sc.line > best.line)
|
|
3573
|
+
best = { line: sc.line };
|
|
3574
|
+
}
|
|
3575
|
+
if (best)
|
|
3576
|
+
out.push({ sinkLine, sourceLine: best.line });
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
return out;
|
|
3581
|
+
}
|
|
3582
|
+
const JSPWIKI_TAG_PKG = 'org.apache.wiki.tags';
|
|
3583
|
+
const JSPWIKI_TAG_SUPER_NAMES = ['WikiTagBase', 'WikiBodyTag', 'WikiLinkTag'];
|
|
3584
|
+
/**
|
|
3585
|
+
* cognium-dev#624 (WikiTag slice) — JSPWiki tag handlers inherit `protected
|
|
3586
|
+
* WikiContext m_wikiContext` from `WikiTagBase`. The configured
|
|
3587
|
+
* `WikiContext.getPage` source is class-scoped, so `m_wikiContext.getPage()`
|
|
3588
|
+
* misses unless the enclosing type extends a JSPWiki tag base (import-gated,
|
|
3589
|
+
* never from the field name alone).
|
|
3590
|
+
*/
|
|
3591
|
+
export function javaWikiTagInheritedContextReceiver(receiver, className, call, types, imports) {
|
|
3592
|
+
if (className !== 'WikiContext' || receiver !== 'm_wikiContext')
|
|
3593
|
+
return false;
|
|
3594
|
+
const line = call.location.line;
|
|
3595
|
+
const enclosing = types.find(t => line >= t.start_line && line <= t.end_line &&
|
|
3596
|
+
(!call.in_method || t.methods.some(m => m.name === call.in_method && line >= m.start_line && line <= m.end_line)));
|
|
3597
|
+
if (!enclosing?.extends)
|
|
3598
|
+
return false;
|
|
3599
|
+
const importedSimple = new Set();
|
|
3600
|
+
let wildcardImported = false;
|
|
3601
|
+
for (const i of imports) {
|
|
3602
|
+
if (i.from_package !== JSPWIKI_TAG_PKG)
|
|
3603
|
+
continue;
|
|
3604
|
+
if (i.is_wildcard)
|
|
3605
|
+
wildcardImported = true;
|
|
3606
|
+
else if (JSPWIKI_TAG_SUPER_NAMES.includes(i.imported_name)) {
|
|
3607
|
+
importedSimple.add(i.imported_name);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const isTagSuper = (name) => {
|
|
3611
|
+
const bare = name.replace(/<.*$/, '').trim();
|
|
3612
|
+
if (bare === `${JSPWIKI_TAG_PKG}.WikiTagBase` ||
|
|
3613
|
+
bare === `${JSPWIKI_TAG_PKG}.WikiBodyTag` ||
|
|
3614
|
+
bare === `${JSPWIKI_TAG_PKG}.WikiLinkTag`)
|
|
3615
|
+
return true;
|
|
3616
|
+
return JSPWIKI_TAG_SUPER_NAMES.includes(bare) &&
|
|
3617
|
+
(wildcardImported || importedSimple.has(bare));
|
|
3618
|
+
};
|
|
3619
|
+
return isTagSuper(enclosing.extends);
|
|
3620
|
+
}
|
|
2630
3621
|
export function findPythonReturnXSSSinks(sourceCode, taintedVars) {
|
|
2631
3622
|
if (taintedVars.size === 0)
|
|
2632
3623
|
return [];
|
|
@@ -2811,6 +3802,7 @@ export function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
2811
3802
|
return new Map();
|
|
2812
3803
|
const tainted = new Map();
|
|
2813
3804
|
const lines = sourceCode.split('\n');
|
|
3805
|
+
const lookupTables = constLookupTables(sourceCode, language);
|
|
2814
3806
|
// Seed with WebSocket `.on('message', cb)` callback-param names (#213
|
|
2815
3807
|
// second slice). Same event allowlist as findJavaScriptCallbackParamSources.
|
|
2816
3808
|
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
@@ -2834,6 +3826,8 @@ export function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
2834
3826
|
const [, lhs, rhs] = assignMatch;
|
|
2835
3827
|
if (['if', 'while', 'for', 'return', 'true', 'false', 'null', 'undefined', 'case'].includes(lhs))
|
|
2836
3828
|
continue;
|
|
3829
|
+
if (isConstTableLookup(rhs, lookupTables))
|
|
3830
|
+
continue; // #667
|
|
2837
3831
|
const isDirectSource = JS_TAINTED_PATTERNS.some(p => p.pattern.test(rhs));
|
|
2838
3832
|
const isTaintedPropagation = tainted.size > 0 && [...tainted.keys()].some(v => new RegExp(`\\b${v}\\b`).test(rhs));
|
|
2839
3833
|
if (isDirectSource || isTaintedPropagation)
|
|
@@ -2891,6 +3885,43 @@ export function buildRustTaintedVars(sourceCode, seedVars) {
|
|
|
2891
3885
|
}
|
|
2892
3886
|
return derived;
|
|
2893
3887
|
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Join an assignment / declaration whose call argument list wraps over several
|
|
3890
|
+
* lines into one line (`label = String.format("%s.tar",\n picked);`), or null
|
|
3891
|
+
* when line `i` does not start such a statement. The statement must start with
|
|
3892
|
+
* `[Type] name =`, leave a parenthesis open on its first line, and close it and
|
|
3893
|
+
* end in `;` within a few lines, with no `{`, `}`, lambda arrow or second `;`
|
|
3894
|
+
* on the way (a lambda, anonymous class, switch expression or block is not a
|
|
3895
|
+
* plain wrapped value).
|
|
3896
|
+
*/
|
|
3897
|
+
function joinWrappedJavaStatement(lines, i) {
|
|
3898
|
+
const strip = (l) => l.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)'/g, '""').replace(/\/\/.*$/, '');
|
|
3899
|
+
const first = strip(lines[i]);
|
|
3900
|
+
if (!/^\s*(?:(?:final\s+)?[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(first))
|
|
3901
|
+
return null;
|
|
3902
|
+
if (/[;{}]|->/.test(first))
|
|
3903
|
+
return null;
|
|
3904
|
+
const depthOf = (t) => (t.match(/\(/g)?.length ?? 0) - (t.match(/\)/g)?.length ?? 0);
|
|
3905
|
+
let depth = depthOf(first);
|
|
3906
|
+
if (depth <= 0)
|
|
3907
|
+
return null;
|
|
3908
|
+
let text = lines[i].replace(/\/\/.*$/, '');
|
|
3909
|
+
for (let j = i + 1; j < lines.length && j <= i + 12; j++) {
|
|
3910
|
+
const next = strip(lines[j]);
|
|
3911
|
+
if (/[{}]|->/.test(next))
|
|
3912
|
+
return null;
|
|
3913
|
+
const semi = next.indexOf(';');
|
|
3914
|
+
if (semi >= 0 && next.slice(semi + 1).trim() !== '')
|
|
3915
|
+
return null;
|
|
3916
|
+
depth += depthOf(next);
|
|
3917
|
+
if (depth < 0)
|
|
3918
|
+
return null;
|
|
3919
|
+
text += ' ' + lines[j].replace(/\/\/.*$/, '').trim();
|
|
3920
|
+
if (semi >= 0)
|
|
3921
|
+
return depth === 0 && text.trimEnd().endsWith(';') ? text : null;
|
|
3922
|
+
}
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
2894
3925
|
/**
|
|
2895
3926
|
* cognium-dev #220 — Java derived-taint expansion.
|
|
2896
3927
|
*
|
|
@@ -2955,8 +3986,46 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
|
2955
3986
|
const trimmed = line.trimStart();
|
|
2956
3987
|
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
2957
3988
|
continue;
|
|
2958
|
-
|
|
2959
|
-
|
|
3989
|
+
// cognium-dev#476: non-assignment statements (must run before the #753
|
|
3990
|
+
// wrap-join early-continue, which would skip them).
|
|
3991
|
+
const arrayCopyRe = /\bSystem\.arraycopy\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^,]+\s*,\s*([A-Za-z_]\w*)\s*,/;
|
|
3992
|
+
const acMatch = arrayCopyRe.exec(line);
|
|
3993
|
+
if (acMatch) {
|
|
3994
|
+
const srcArr = acMatch[1];
|
|
3995
|
+
const dstArr = acMatch[2];
|
|
3996
|
+
if (knownTainted.has(srcArr) && !knownTainted.has(dstArr)) {
|
|
3997
|
+
derived.set(dstArr, i + 1);
|
|
3998
|
+
knownTainted.add(dstArr);
|
|
3999
|
+
changed = true;
|
|
4000
|
+
}
|
|
4001
|
+
continue;
|
|
4002
|
+
}
|
|
4003
|
+
const listAddRe = /^\s*([A-Za-z_]\w*)\.add\s*\(\s*([A-Za-z_]\w*)\s*\)\s*;?\s*$/;
|
|
4004
|
+
const addMatch = listAddRe.exec(line.trimEnd());
|
|
4005
|
+
if (addMatch) {
|
|
4006
|
+
const listVar = addMatch[1];
|
|
4007
|
+
const elem = addMatch[2];
|
|
4008
|
+
if (knownTainted.has(elem) && !knownTainted.has(listVar)) {
|
|
4009
|
+
derived.set(listVar, i + 1);
|
|
4010
|
+
knownTainted.add(listVar);
|
|
4011
|
+
changed = true;
|
|
4012
|
+
}
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
let statement = line;
|
|
4016
|
+
let declMatch = declRe.exec(statement);
|
|
4017
|
+
let assignMatch = !declMatch ? assignRe.exec(statement) : null;
|
|
4018
|
+
if (!declMatch && !assignMatch) {
|
|
4019
|
+
// cognium-dev#753: `label = String.format("%s.tar",\n picked);` — a
|
|
4020
|
+
// statement whose call argument list wraps over several lines reads like
|
|
4021
|
+
// the same statement on one line.
|
|
4022
|
+
const joined = joinWrappedJavaStatement(lines, i);
|
|
4023
|
+
if (joined === null)
|
|
4024
|
+
continue;
|
|
4025
|
+
statement = joined;
|
|
4026
|
+
declMatch = declRe.exec(statement);
|
|
4027
|
+
assignMatch = !declMatch ? assignRe.exec(statement) : null;
|
|
4028
|
+
}
|
|
2960
4029
|
const m = declMatch ?? assignMatch;
|
|
2961
4030
|
if (!m)
|
|
2962
4031
|
continue;
|
|
@@ -3825,9 +4894,11 @@ function findBashRealpathPrefixGuardSanitizers(code) {
|
|
|
3825
4894
|
function findGoMapAllowlistGuardSanitizers(code) {
|
|
3826
4895
|
const sanitizers = [];
|
|
3827
4896
|
const lines = code.split('\n');
|
|
3828
|
-
// `if !<mapName>[<key>] {
|
|
3829
|
-
//
|
|
3830
|
-
|
|
4897
|
+
// `if !<mapName>[<key>] {`, or that negated lookup as one `||` disjunct
|
|
4898
|
+
// (`if err != nil || !<mapName>[u.Host] {`, #293): the block runs when any
|
|
4899
|
+
// disjunct holds, so falling past it proves the key is in the map. The key
|
|
4900
|
+
// may be a selector (`u.Host`). Captures the map identifier in group 1.
|
|
4901
|
+
const guardOpen = /^\s*if\s+(?:[^{&|]+\|\|\s*)*!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\[\s*[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?\s*\]\s*(?:\|\|[^{&]+)?\{/;
|
|
3831
4902
|
// Allow-list naming heuristic.
|
|
3832
4903
|
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
3833
4904
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3863,9 +4934,24 @@ function findGoMapAllowlistGuardSanitizers(code) {
|
|
|
3863
4934
|
if (closeLine === -1 || !bodyHasTerminator)
|
|
3864
4935
|
continue;
|
|
3865
4936
|
// Emit per-line sanitizers for every line AFTER the guard's closing
|
|
3866
|
-
// brace
|
|
3867
|
-
//
|
|
3868
|
-
|
|
4937
|
+
// brace to the end of the enclosing block (the function body, #293 —
|
|
4938
|
+
// it used to run to the end of the file, crediting later functions).
|
|
4939
|
+
// `checkSanitized` is line-keyed so this suppresses the relevant sink
|
|
4940
|
+
// types in the safe branch.
|
|
4941
|
+
let end = lines.length;
|
|
4942
|
+
for (let j = closeLine + 1, d = 0; j < lines.length; j++) {
|
|
4943
|
+
for (const ch of lines[j]) {
|
|
4944
|
+
if (ch === '{')
|
|
4945
|
+
d++;
|
|
4946
|
+
else if (ch === '}')
|
|
4947
|
+
d--;
|
|
4948
|
+
}
|
|
4949
|
+
if (d < 0) {
|
|
4950
|
+
end = j + 1;
|
|
4951
|
+
break;
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
for (let l = closeLine + 2; l <= end; l++) {
|
|
3869
4955
|
sanitizers.push({
|
|
3870
4956
|
type: 'go_map_allowlist_guard',
|
|
3871
4957
|
method: 'if',
|
|
@@ -4235,59 +5321,249 @@ function findPythonRegexAllowlistWrapperSanitizers(code) {
|
|
|
4235
5321
|
const line = lines[i];
|
|
4236
5322
|
if (!bareCallRe.test(line))
|
|
4237
5323
|
continue;
|
|
4238
|
-
// Skip calls inside the wrapper's own body.
|
|
4239
|
-
if (i > w.defLine) {
|
|
4240
|
-
const lineIndent = line.length - line.trimStart().length;
|
|
4241
|
-
if (lineIndent > w.defIndent) {
|
|
4242
|
-
let stillInside = true;
|
|
4243
|
-
for (let k = w.defLine + 1; k < i; k++) {
|
|
4244
|
-
const kline = lines[k];
|
|
4245
|
-
if (kline.trim() === '')
|
|
4246
|
-
continue;
|
|
4247
|
-
const kindent = kline.length - kline.trimStart().length;
|
|
4248
|
-
if (kindent <= w.defIndent) {
|
|
4249
|
-
stillInside = false;
|
|
4250
|
-
break;
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
if (stillInside)
|
|
4254
|
-
continue;
|
|
4255
|
-
}
|
|
5324
|
+
// Skip calls inside the wrapper's own body.
|
|
5325
|
+
if (i > w.defLine) {
|
|
5326
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
5327
|
+
if (lineIndent > w.defIndent) {
|
|
5328
|
+
let stillInside = true;
|
|
5329
|
+
for (let k = w.defLine + 1; k < i; k++) {
|
|
5330
|
+
const kline = lines[k];
|
|
5331
|
+
if (kline.trim() === '')
|
|
5332
|
+
continue;
|
|
5333
|
+
const kindent = kline.length - kline.trimStart().length;
|
|
5334
|
+
if (kindent <= w.defIndent) {
|
|
5335
|
+
stillInside = false;
|
|
5336
|
+
break;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
if (stillInside)
|
|
5340
|
+
continue;
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
// Emit a sanitizer at the call line itself (covers ETE on a
|
|
5344
|
+
// single-line `NAME(req.args.get(...))` shape).
|
|
5345
|
+
sanitizers.push({
|
|
5346
|
+
type: 'python_regex_allowlist_wrapper',
|
|
5347
|
+
method: w.name,
|
|
5348
|
+
line: i + 1,
|
|
5349
|
+
sanitizes: [...wrapperKinds],
|
|
5350
|
+
});
|
|
5351
|
+
// If the call result is assigned to a variable, track it and
|
|
5352
|
+
// emit sanitizers on every later line in the same function
|
|
5353
|
+
// block that mentions that variable name. End the scan when
|
|
5354
|
+
// we leave the call's enclosing block.
|
|
5355
|
+
const am = assignedCallRe.exec(line);
|
|
5356
|
+
if (!am)
|
|
5357
|
+
continue;
|
|
5358
|
+
const callIndent = am[1].length;
|
|
5359
|
+
const validated = am[2];
|
|
5360
|
+
const validatedRe = new RegExp(`\\b${validated}\\b`);
|
|
5361
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
5362
|
+
const jline = lines[j];
|
|
5363
|
+
if (jline.trim() === '')
|
|
5364
|
+
continue;
|
|
5365
|
+
const jindent = jline.length - jline.trimStart().length;
|
|
5366
|
+
if (jindent < callIndent)
|
|
5367
|
+
break;
|
|
5368
|
+
if (validatedRe.test(jline)) {
|
|
5369
|
+
sanitizers.push({
|
|
5370
|
+
type: 'python_regex_allowlist_wrapper',
|
|
5371
|
+
method: w.name,
|
|
5372
|
+
line: j + 1,
|
|
5373
|
+
sanitizes: [...wrapperKinds],
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
return sanitizers;
|
|
5380
|
+
}
|
|
5381
|
+
function isPythonQuotedStringLiteral(token) {
|
|
5382
|
+
const t = token.trim();
|
|
5383
|
+
return (/^"(?:[^"\\]|\\.)*"$/.test(t) ||
|
|
5384
|
+
/^'(?:[^'\\]|\\.)*'$/.test(t));
|
|
5385
|
+
}
|
|
5386
|
+
function pythonRhsIsLiteralStringCollection(rhs) {
|
|
5387
|
+
let s = rhs.trim();
|
|
5388
|
+
if (/^frozenset\s*\(/.test(s) && s.endsWith(')')) {
|
|
5389
|
+
s = s.slice(s.indexOf('(') + 1, -1).trim();
|
|
5390
|
+
}
|
|
5391
|
+
const splitElems = (inner) => {
|
|
5392
|
+
const parts = [];
|
|
5393
|
+
let depth = 0;
|
|
5394
|
+
let quote = null;
|
|
5395
|
+
let start = 0;
|
|
5396
|
+
for (let i = 0; i < inner.length; i++) {
|
|
5397
|
+
const c = inner[i];
|
|
5398
|
+
if (quote) {
|
|
5399
|
+
if (c === '\\') {
|
|
5400
|
+
i++;
|
|
5401
|
+
continue;
|
|
5402
|
+
}
|
|
5403
|
+
if (c === quote)
|
|
5404
|
+
quote = null;
|
|
5405
|
+
continue;
|
|
5406
|
+
}
|
|
5407
|
+
if (c === '"' || c === "'") {
|
|
5408
|
+
quote = c;
|
|
5409
|
+
continue;
|
|
5410
|
+
}
|
|
5411
|
+
if (c === '(' || c === '[' || c === '{')
|
|
5412
|
+
depth++;
|
|
5413
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
5414
|
+
depth--;
|
|
5415
|
+
else if (c === ',' && depth === 0) {
|
|
5416
|
+
parts.push(inner.slice(start, i).trim());
|
|
5417
|
+
start = i + 1;
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
const last = inner.slice(start).trim();
|
|
5421
|
+
if (last || parts.length > 0)
|
|
5422
|
+
parts.push(last);
|
|
5423
|
+
return parts.filter(Boolean);
|
|
5424
|
+
};
|
|
5425
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
5426
|
+
const inner = s.slice(1, -1).trim();
|
|
5427
|
+
if (!inner)
|
|
5428
|
+
return true;
|
|
5429
|
+
return splitElems(inner).every(isPythonQuotedStringLiteral);
|
|
5430
|
+
}
|
|
5431
|
+
if (s.startsWith('[') && s.endsWith(']')) {
|
|
5432
|
+
const inner = s.slice(1, -1).trim();
|
|
5433
|
+
if (!inner)
|
|
5434
|
+
return false;
|
|
5435
|
+
return splitElems(inner).every(isPythonQuotedStringLiteral);
|
|
5436
|
+
}
|
|
5437
|
+
if (s.startsWith('(') && s.endsWith(')')) {
|
|
5438
|
+
const inner = s.slice(1, -1).trim();
|
|
5439
|
+
if (!inner || !inner.includes(','))
|
|
5440
|
+
return false;
|
|
5441
|
+
return splitElems(inner).every(isPythonQuotedStringLiteral);
|
|
5442
|
+
}
|
|
5443
|
+
return false;
|
|
5444
|
+
}
|
|
5445
|
+
function collectPythonLiteralMembershipCollections(code) {
|
|
5446
|
+
const names = new Set();
|
|
5447
|
+
const lines = code.split('\n');
|
|
5448
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5449
|
+
const raw = lines[i];
|
|
5450
|
+
const trimmed = raw.trim();
|
|
5451
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
5452
|
+
continue;
|
|
5453
|
+
const indent = raw.length - raw.trimStart().length;
|
|
5454
|
+
if (indent !== 0)
|
|
5455
|
+
continue;
|
|
5456
|
+
const am = /^([A-Za-z_]\w*)\s*=\s*(.+)$/.exec(trimmed);
|
|
5457
|
+
if (!am)
|
|
5458
|
+
continue;
|
|
5459
|
+
const name = am[1];
|
|
5460
|
+
if (!pythonRhsIsLiteralStringCollection(am[2]))
|
|
5461
|
+
continue;
|
|
5462
|
+
if (new RegExp(String.raw `\b${name}\s*(?:\.add\s*\(|\.update\s*\(|\|=)|\b${name}\s*\[[^\]]+\]\s*=`).test(code)) {
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
names.add(name);
|
|
5466
|
+
}
|
|
5467
|
+
return names;
|
|
5468
|
+
}
|
|
5469
|
+
function pythonEnclosingDefEndLine(lines, fromLine) {
|
|
5470
|
+
let defLine = -1;
|
|
5471
|
+
let defIndent = -1;
|
|
5472
|
+
for (let k = fromLine; k >= 0; k--) {
|
|
5473
|
+
const dm = /^(\s*)def\s+/.exec(lines[k]);
|
|
5474
|
+
if (dm) {
|
|
5475
|
+
defLine = k;
|
|
5476
|
+
defIndent = dm[1].length;
|
|
5477
|
+
break;
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
if (defLine < 0)
|
|
5481
|
+
return lines.length;
|
|
5482
|
+
for (let j = defLine + 1; j < lines.length; j++) {
|
|
5483
|
+
const line = lines[j];
|
|
5484
|
+
if (line.trim() === '')
|
|
5485
|
+
continue;
|
|
5486
|
+
const indent = line.length - line.trimStart().length;
|
|
5487
|
+
if (indent <= defIndent && /^\s*(?:def\s+|@|class\s+)/.test(line)) {
|
|
5488
|
+
return j;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
return lines.length;
|
|
5492
|
+
}
|
|
5493
|
+
/**
|
|
5494
|
+
* Python literal set/list/tuple membership guard (cognium-dev #563).
|
|
5495
|
+
*
|
|
5496
|
+
* if theme not in THEMES:
|
|
5497
|
+
* abort(400)
|
|
5498
|
+
* session["theme"] = theme
|
|
5499
|
+
*
|
|
5500
|
+
* Past the guard, `theme` is one of the module-level string literals in
|
|
5501
|
+
* `THEMES`. The collection must be a literal of string constants that the
|
|
5502
|
+
* file never mutates.
|
|
5503
|
+
*/
|
|
5504
|
+
function findPythonLiteralMembershipGuardSanitizers(code) {
|
|
5505
|
+
const sanitizers = [];
|
|
5506
|
+
const lines = code.split('\n');
|
|
5507
|
+
const literalCollections = collectPythonLiteralMembershipCollections(code);
|
|
5508
|
+
if (literalCollections.size === 0)
|
|
5509
|
+
return sanitizers;
|
|
5510
|
+
const guardOpen = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
|
|
5511
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
5512
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5513
|
+
const m = guardOpen.exec(lines[i]);
|
|
5514
|
+
if (!m)
|
|
5515
|
+
continue;
|
|
5516
|
+
const guardIndent = m[1].length;
|
|
5517
|
+
const guardedVar = m[2];
|
|
5518
|
+
const collectionName = m[3];
|
|
5519
|
+
if (!literalCollections.has(collectionName))
|
|
5520
|
+
continue;
|
|
5521
|
+
let bodyHasTerminator = false;
|
|
5522
|
+
let blockEnd = -1;
|
|
5523
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
5524
|
+
for (let j = i + 1; j < maxScan; j++) {
|
|
5525
|
+
const line = lines[j];
|
|
5526
|
+
if (line.trim() === '')
|
|
5527
|
+
continue;
|
|
5528
|
+
const indent = line.length - line.trimStart().length;
|
|
5529
|
+
if (indent <= guardIndent) {
|
|
5530
|
+
blockEnd = j - 1;
|
|
5531
|
+
break;
|
|
4256
5532
|
}
|
|
4257
|
-
|
|
4258
|
-
|
|
5533
|
+
if (terminator.test(line))
|
|
5534
|
+
bodyHasTerminator = true;
|
|
5535
|
+
}
|
|
5536
|
+
if (blockEnd === -1)
|
|
5537
|
+
blockEnd = Math.min(lines.length - 1, i + 25);
|
|
5538
|
+
if (!bodyHasTerminator)
|
|
5539
|
+
continue;
|
|
5540
|
+
const varRe = new RegExp(String.raw `\b${guardedVar}\b`);
|
|
5541
|
+
const reassign = new RegExp(String.raw `^\s*${guardedVar}\s*=(?!=)`);
|
|
5542
|
+
const funcEnd = pythonEnclosingDefEndLine(lines, i);
|
|
5543
|
+
for (let l = blockEnd + 2; l <= funcEnd; l++) {
|
|
5544
|
+
const lineText = lines[l - 1];
|
|
5545
|
+
if (!lineText || !varRe.test(lineText))
|
|
5546
|
+
continue;
|
|
5547
|
+
if (reassign.test(lineText))
|
|
5548
|
+
break;
|
|
4259
5549
|
sanitizers.push({
|
|
4260
|
-
type: '
|
|
4261
|
-
method:
|
|
4262
|
-
line:
|
|
4263
|
-
sanitizes: [
|
|
5550
|
+
type: 'python_literal_membership_guard',
|
|
5551
|
+
method: 'if',
|
|
5552
|
+
line: l,
|
|
5553
|
+
sanitizes: [
|
|
5554
|
+
'trust_boundary',
|
|
5555
|
+
'sql_injection',
|
|
5556
|
+
'command_injection',
|
|
5557
|
+
'path_traversal',
|
|
5558
|
+
'ssrf',
|
|
5559
|
+
'ldap_injection',
|
|
5560
|
+
'xpath_injection',
|
|
5561
|
+
'log_injection',
|
|
5562
|
+
'xss',
|
|
5563
|
+
'open_redirect',
|
|
5564
|
+
'external_taint_escape',
|
|
5565
|
+
],
|
|
4264
5566
|
});
|
|
4265
|
-
// If the call result is assigned to a variable, track it and
|
|
4266
|
-
// emit sanitizers on every later line in the same function
|
|
4267
|
-
// block that mentions that variable name. End the scan when
|
|
4268
|
-
// we leave the call's enclosing block.
|
|
4269
|
-
const am = assignedCallRe.exec(line);
|
|
4270
|
-
if (!am)
|
|
4271
|
-
continue;
|
|
4272
|
-
const callIndent = am[1].length;
|
|
4273
|
-
const validated = am[2];
|
|
4274
|
-
const validatedRe = new RegExp(`\\b${validated}\\b`);
|
|
4275
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
4276
|
-
const jline = lines[j];
|
|
4277
|
-
if (jline.trim() === '')
|
|
4278
|
-
continue;
|
|
4279
|
-
const jindent = jline.length - jline.trimStart().length;
|
|
4280
|
-
if (jindent < callIndent)
|
|
4281
|
-
break;
|
|
4282
|
-
if (validatedRe.test(jline)) {
|
|
4283
|
-
sanitizers.push({
|
|
4284
|
-
type: 'python_regex_allowlist_wrapper',
|
|
4285
|
-
method: w.name,
|
|
4286
|
-
line: j + 1,
|
|
4287
|
-
sanitizes: [...wrapperKinds],
|
|
4288
|
-
});
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
5567
|
}
|
|
4292
5568
|
}
|
|
4293
5569
|
return sanitizers;
|
|
@@ -5677,6 +6953,11 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
5677
6953
|
continue;
|
|
5678
6954
|
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
5679
6955
|
continue;
|
|
6956
|
+
// cognium-dev#618: a string-prefix check against a canonical base with no
|
|
6957
|
+
// trailing separator is the prefix-confusion bypass, not a guard.
|
|
6958
|
+
const base = javaCallArgument(lines.slice(i).join('\n'), m.index + m[0].length - 1);
|
|
6959
|
+
if (base !== null && javaCanonicalBaseLacksSeparator(lines, i, base))
|
|
6960
|
+
continue;
|
|
5680
6961
|
const guarded = new Set([m[1]]);
|
|
5681
6962
|
const mentions = (t) => {
|
|
5682
6963
|
for (const n of guarded)
|
|
@@ -5710,6 +6991,147 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
5710
6991
|
}
|
|
5711
6992
|
return sanitizers;
|
|
5712
6993
|
}
|
|
6994
|
+
/**
|
|
6995
|
+
* Identifiers allowed in the initializer of a constant allowlist collection.
|
|
6996
|
+
* Anything else (a method parameter, a request read, a config lookup) means
|
|
6997
|
+
* the membership set is not a fixed set of literals.
|
|
6998
|
+
*/
|
|
6999
|
+
const JAVA_LITERAL_COLLECTION_IDENTS = new Set([
|
|
7000
|
+
'new', 'String', 'Set', 'List', 'of', 'copyOf', 'Arrays', 'asList',
|
|
7001
|
+
'HashSet', 'TreeSet', 'LinkedHashSet', 'ArrayList', 'LinkedList',
|
|
7002
|
+
'Collections', 'unmodifiableSet', 'unmodifiableList',
|
|
7003
|
+
'ImmutableSet', 'ImmutableList', 'java', 'util',
|
|
7004
|
+
]);
|
|
7005
|
+
/**
|
|
7006
|
+
* cognium-dev#668 — Java reflection allowlist guard.
|
|
7007
|
+
*
|
|
7008
|
+
* private static final Set<String> ALLOWED = Set.of("a.Csv", "a.Json");
|
|
7009
|
+
* if (!ALLOWED.contains(cls)) { throw new SecurityException("denied"); }
|
|
7010
|
+
* return Class.forName(cls).getDeclaredConstructor().newInstance();
|
|
7011
|
+
*
|
|
7012
|
+
* Past a rejecting `!<SET>.contains(<v>)` guard, `<v>` is one of the set's
|
|
7013
|
+
* literals, so the class it names is chosen by the code, not the requester.
|
|
7014
|
+
* Positive evidence only: `<SET>` must be a `final` field or local of this
|
|
7015
|
+
* file whose initializer is nothing but string literals inside the standard
|
|
7016
|
+
* collection factories, and the guard must reject (throw / return). Credits
|
|
7017
|
+
* `<v>` on later lines of the enclosing method until `<v>` is reassigned.
|
|
7018
|
+
* Scoped to `code_injection` (reflective class loading); other sink types
|
|
7019
|
+
* are untouched.
|
|
7020
|
+
*/
|
|
7021
|
+
function findJavaReflectionAllowlistGuardSanitizers(code, types) {
|
|
7022
|
+
const sanitizers = [];
|
|
7023
|
+
const lines = code.split('\n');
|
|
7024
|
+
const guardRe = /\bif\s*\(\s*!\s*(?:this\s*\.\s*|[A-Z]\w*\s*\.\s*)?([A-Za-z_]\w*)\s*\.\s*contains\s*\(\s*([A-Za-z_]\w*)\s*\)\s*\)/;
|
|
7025
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
7026
|
+
// Factory / constructor tokens that prove the initializer is a collection of
|
|
7027
|
+
// literals. A bare `final String ALLOWED = "…"` leaves no idents after string
|
|
7028
|
+
// stripping (so `.every` would pass) and `String.contains` is substring — not
|
|
7029
|
+
// an allowlist. Require at least one of these.
|
|
7030
|
+
const LITERAL_COLLECTION_FACTORIES = new Set([
|
|
7031
|
+
'of', 'copyOf', 'asList', 'HashSet', 'TreeSet', 'LinkedHashSet',
|
|
7032
|
+
'ArrayList', 'LinkedList', 'unmodifiableSet', 'unmodifiableList',
|
|
7033
|
+
'ImmutableSet', 'ImmutableList',
|
|
7034
|
+
]);
|
|
7035
|
+
/**
|
|
7036
|
+
* Nearest declaration of `name` at or above `fromLine` (0-based). A closer
|
|
7037
|
+
* non-final binding (local, `var`, parameter) wins over an earlier `final`
|
|
7038
|
+
* field; only a nearest `final` literal collection returns true.
|
|
7039
|
+
*/
|
|
7040
|
+
const isLiteralCollection = (name, fromLine) => {
|
|
7041
|
+
const finalDeclRe = new RegExp(`\\bfinal\\b[^=;(]*\\b${name}\\s*=(?!=)`);
|
|
7042
|
+
// Type/var declaration or use as a parameter (`Type name` before `=` / `;` / `,` / `)`).
|
|
7043
|
+
const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]?\\s]*|var)\\s+${name}\\s*[=;,)]`);
|
|
7044
|
+
for (let i = fromLine; i >= 0; i--) {
|
|
7045
|
+
const line = lines[i];
|
|
7046
|
+
if (!anyDeclRe.test(line) && !finalDeclRe.test(line))
|
|
7047
|
+
continue;
|
|
7048
|
+
const dm = finalDeclRe.exec(line);
|
|
7049
|
+
if (!dm)
|
|
7050
|
+
return false; // nearer non-final binding — not a fixed allowlist
|
|
7051
|
+
// Initializer up to the terminating `;` (may span a few lines).
|
|
7052
|
+
let init = line.slice(dm.index + dm[0].length);
|
|
7053
|
+
for (let j = i + 1; !init.includes(';') && j < Math.min(lines.length, i + 12); j++)
|
|
7054
|
+
init += ' ' + lines[j];
|
|
7055
|
+
if (!init.includes(';'))
|
|
7056
|
+
return false;
|
|
7057
|
+
init = init.slice(0, init.indexOf(';')).replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
7058
|
+
if (!init.includes('""'))
|
|
7059
|
+
return false;
|
|
7060
|
+
const idents = init.match(/[A-Za-z_]\w*/g) ?? [];
|
|
7061
|
+
// Empty idents (string-only initializer) or no collection factory → not a set.
|
|
7062
|
+
if (!idents.some(id => LITERAL_COLLECTION_FACTORIES.has(id)))
|
|
7063
|
+
return false;
|
|
7064
|
+
if (!idents.every(id => JAVA_LITERAL_COLLECTION_IDENTS.has(id)))
|
|
7065
|
+
return false;
|
|
7066
|
+
// A mutable collection that is written anywhere is not a fixed set.
|
|
7067
|
+
const mutRe = new RegExp(`\\b${name}\\s*\\.\\s*(?:add|addAll|put|putAll|set|replaceAll)\\s*\\(`);
|
|
7068
|
+
return !lines.some(l => mutRe.test(l));
|
|
7069
|
+
}
|
|
7070
|
+
return false;
|
|
7071
|
+
};
|
|
7072
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7073
|
+
const m = guardRe.exec(lines[i]);
|
|
7074
|
+
if (!m)
|
|
7075
|
+
continue;
|
|
7076
|
+
const [, setName, guarded] = m;
|
|
7077
|
+
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
7078
|
+
continue;
|
|
7079
|
+
if (!isLiteralCollection(setName, i))
|
|
7080
|
+
continue;
|
|
7081
|
+
const method = types
|
|
7082
|
+
.flatMap(t => t.methods)
|
|
7083
|
+
.filter(mt => mt.start_line <= i + 1 && mt.end_line >= i + 1)
|
|
7084
|
+
.sort((a, b) => (a.end_line - a.start_line) - (b.end_line - b.start_line))[0];
|
|
7085
|
+
if (!method)
|
|
7086
|
+
continue;
|
|
7087
|
+
// The guard must sit in the method body's top-level block, so it dominates
|
|
7088
|
+
// every later line (not inside an `if` branch, loop or lambda body).
|
|
7089
|
+
const before = [...lines.slice(method.start_line - 1, i), lines[i].slice(0, m.index)]
|
|
7090
|
+
.join('\n').replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
7091
|
+
const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length;
|
|
7092
|
+
if (depth !== 1)
|
|
7093
|
+
continue;
|
|
7094
|
+
// Braceless `if`/`for`/`while`/`else` keeps brace-depth 1, so also reject a
|
|
7095
|
+
// guard that is the body of a preceding braceless control statement — on the
|
|
7096
|
+
// same line (`if (strict) if (!SET.contains(v))`) or the previous statement
|
|
7097
|
+
// (`} else` / `else` / `if (x)` without `{`).
|
|
7098
|
+
const samePrefix = lines[i].slice(0, m.index).replace(/\/\/.*$/, '').trim();
|
|
7099
|
+
let prev = '';
|
|
7100
|
+
for (let p = i - 1; p >= method.start_line - 1; p--) {
|
|
7101
|
+
const t = lines[p].replace(/\/\/.*$/, '').trim();
|
|
7102
|
+
if (!t)
|
|
7103
|
+
continue;
|
|
7104
|
+
prev = t;
|
|
7105
|
+
break;
|
|
7106
|
+
}
|
|
7107
|
+
const openBraceless = (s) => {
|
|
7108
|
+
// Allow a leading `}` so `} else` counts as braceless else.
|
|
7109
|
+
const t = s.replace(/^\}+\s*/, '').trim();
|
|
7110
|
+
return /^(?:else(?:\s+if\b)?|if|for|while)\b/.test(t) && !s.includes('{');
|
|
7111
|
+
};
|
|
7112
|
+
if (openBraceless(samePrefix) || (prev && openBraceless(prev)))
|
|
7113
|
+
continue;
|
|
7114
|
+
const refRe = new RegExp(`(?<![\\w.])${guarded}\\b`);
|
|
7115
|
+
// Credit only lines where `<v>` is a whole call argument, `f(<v>)`, so a
|
|
7116
|
+
// second tainted value on the same line (`forName(v + other)`) still reports.
|
|
7117
|
+
const wholeArgRe = new RegExp(`\\(\\s*${guarded}\\s*\\)`);
|
|
7118
|
+
const reassignRe = new RegExp(`(?<![\\w.])${guarded}\\s*(?:=(?!=)|\\+=)`);
|
|
7119
|
+
for (let l = i + 1; l < Math.min(method.end_line, lines.length); l++) {
|
|
7120
|
+
const lt = lines[l];
|
|
7121
|
+
if (reassignRe.test(lt))
|
|
7122
|
+
break;
|
|
7123
|
+
if (!refRe.test(lt) || !wholeArgRe.test(lt))
|
|
7124
|
+
continue;
|
|
7125
|
+
sanitizers.push({
|
|
7126
|
+
type: 'java_reflection_allowlist_guard',
|
|
7127
|
+
method: 'contains',
|
|
7128
|
+
line: l + 1,
|
|
7129
|
+
sanitizes: ['code_injection'],
|
|
7130
|
+
});
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
return sanitizers;
|
|
7134
|
+
}
|
|
5713
7135
|
/**
|
|
5714
7136
|
* Does the `if (...)` guard on `lines[i]` actually **reject**?
|
|
5715
7137
|
*
|
|
@@ -5785,6 +7207,212 @@ function guardRejects(lines, i, terminatorRe, window) {
|
|
|
5785
7207
|
* form `if (x.StartsWith(root)) { … }` still reports, and a test pins that so
|
|
5786
7208
|
* the gap stays visible rather than silently assumed.
|
|
5787
7209
|
*/
|
|
7210
|
+
/**
|
|
7211
|
+
* cognium-dev#293 (Java cell): membership in a literal allow-list followed by
|
|
7212
|
+
* a reject is a guard.
|
|
7213
|
+
*
|
|
7214
|
+
* if (!Set.of("locale", "theme").contains(key)) throw new IllegalArgumentException();
|
|
7215
|
+
* req.getSession().setAttribute(key, value); // was trust_boundary on `key`
|
|
7216
|
+
*
|
|
7217
|
+
* Past the guard, `key` is one of the listed string constants. The set must
|
|
7218
|
+
* be written out as string literals: inline `Set.of` / `List.of` /
|
|
7219
|
+
* `Arrays.asList`, or a `static final` constant of the same file initialised
|
|
7220
|
+
* from one of them. A set built any other way (a parameter, a config read, a
|
|
7221
|
+
* mutable collection) is not credited: its contents are unknown.
|
|
7222
|
+
*
|
|
7223
|
+
* Only the guarded variable is credited, from the guard to the end of the
|
|
7224
|
+
* enclosing method or the variable's next reassignment, and only when the
|
|
7225
|
+
* guard rejects (`guardRejects`).
|
|
7226
|
+
*/
|
|
7227
|
+
const JAVA_OPERAND_KEYWORDS = new Set([
|
|
7228
|
+
'new', 'return', 'this', 'super', 'null', 'true', 'false', 'throw', 'if', 'else', 'instanceof',
|
|
7229
|
+
'int', 'long', 'short', 'byte', 'char', 'boolean', 'double', 'float', 'var', 'final',
|
|
7230
|
+
]);
|
|
7231
|
+
/**
|
|
7232
|
+
* True when `expr` is only `name`, string literals, `.length()` / `.size()`,
|
|
7233
|
+
* and `+` (no other locals or value-producing calls).
|
|
7234
|
+
*/
|
|
7235
|
+
function javaExprOnlyGuardedAndLits(expr, name) {
|
|
7236
|
+
let s = expr
|
|
7237
|
+
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '')
|
|
7238
|
+
.replace(/\.\s*(?:length|size)\s*\(\s*\)/g, '')
|
|
7239
|
+
.replace(new RegExp(String.raw `\b${name}\b`, 'g'), '');
|
|
7240
|
+
s = s.replace(/[+\s()]/g, '');
|
|
7241
|
+
return s.length === 0;
|
|
7242
|
+
}
|
|
7243
|
+
/** Top-level comma-separated arguments of a call, respecting quotes and nesting. */
|
|
7244
|
+
function javaSplitCallArgs(argsText) {
|
|
7245
|
+
const out = [];
|
|
7246
|
+
let depth = 0;
|
|
7247
|
+
let quote = null;
|
|
7248
|
+
let start = 0;
|
|
7249
|
+
for (let i = 0; i < argsText.length; i++) {
|
|
7250
|
+
const c = argsText[i];
|
|
7251
|
+
if (quote) {
|
|
7252
|
+
if (c === '\\') {
|
|
7253
|
+
i++;
|
|
7254
|
+
continue;
|
|
7255
|
+
}
|
|
7256
|
+
if (c === quote)
|
|
7257
|
+
quote = null;
|
|
7258
|
+
continue;
|
|
7259
|
+
}
|
|
7260
|
+
if (c === '"' || c === "'") {
|
|
7261
|
+
quote = c;
|
|
7262
|
+
continue;
|
|
7263
|
+
}
|
|
7264
|
+
if (c === '(' || c === '[' || c === '{')
|
|
7265
|
+
depth++;
|
|
7266
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
7267
|
+
depth--;
|
|
7268
|
+
else if (c === ',' && depth === 0) {
|
|
7269
|
+
out.push(argsText.slice(start, i).trim());
|
|
7270
|
+
start = i + 1;
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
const last = argsText.slice(start).trim();
|
|
7274
|
+
if (last || out.length > 0)
|
|
7275
|
+
out.push(last);
|
|
7276
|
+
return out;
|
|
7277
|
+
}
|
|
7278
|
+
/**
|
|
7279
|
+
* True when a call on `line` (other than length/size) has an argument that is
|
|
7280
|
+
* not only string literals and the guarded variable — a raw operand.
|
|
7281
|
+
*/
|
|
7282
|
+
function javaCallHasRawArgument(line, name) {
|
|
7283
|
+
const re = /\b([A-Za-z_]\w*)\s*\(/g;
|
|
7284
|
+
let m;
|
|
7285
|
+
while ((m = re.exec(line)) !== null) {
|
|
7286
|
+
const method = m[1];
|
|
7287
|
+
if (method === 'length' || method === 'size')
|
|
7288
|
+
continue;
|
|
7289
|
+
const open = m.index + m[0].length - 1;
|
|
7290
|
+
let depth = 0;
|
|
7291
|
+
let quote = null;
|
|
7292
|
+
let end = -1;
|
|
7293
|
+
for (let i = open; i < line.length; i++) {
|
|
7294
|
+
const c = line[i];
|
|
7295
|
+
if (quote) {
|
|
7296
|
+
if (c === '\\') {
|
|
7297
|
+
i++;
|
|
7298
|
+
continue;
|
|
7299
|
+
}
|
|
7300
|
+
if (c === quote)
|
|
7301
|
+
quote = null;
|
|
7302
|
+
continue;
|
|
7303
|
+
}
|
|
7304
|
+
if (c === '"' || c === "'") {
|
|
7305
|
+
quote = c;
|
|
7306
|
+
continue;
|
|
7307
|
+
}
|
|
7308
|
+
if (c === '(')
|
|
7309
|
+
depth++;
|
|
7310
|
+
else if (c === ')') {
|
|
7311
|
+
depth--;
|
|
7312
|
+
if (depth === 0) {
|
|
7313
|
+
end = i;
|
|
7314
|
+
break;
|
|
7315
|
+
}
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
if (end < 0)
|
|
7319
|
+
continue;
|
|
7320
|
+
for (const arg of javaSplitCallArgs(line.slice(open + 1, end))) {
|
|
7321
|
+
if (!arg)
|
|
7322
|
+
continue;
|
|
7323
|
+
if (!javaExprOnlyGuardedAndLits(arg, name))
|
|
7324
|
+
return true;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
return false;
|
|
7328
|
+
}
|
|
7329
|
+
/**
|
|
7330
|
+
* True when `line` reads a lower-case identifier other than `name` as a value
|
|
7331
|
+
* (not a receiver, call name, member, type, constant or the assigned local),
|
|
7332
|
+
* or a call whose argument is not only string literals / `name` (a call-shaped
|
|
7333
|
+
* raw operand such as `req.getParameter("level")` beside the guarded local).
|
|
7334
|
+
*/
|
|
7335
|
+
function otherLocalOperand(line, name) {
|
|
7336
|
+
if (javaCallHasRawArgument(line, name))
|
|
7337
|
+
return true;
|
|
7338
|
+
const s = line
|
|
7339
|
+
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""')
|
|
7340
|
+
.replace(/^\s*(?:[\w<>[\],.?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/, ' ')
|
|
7341
|
+
.replace(/\.\s*[A-Za-z_]\w*/g, ' ')
|
|
7342
|
+
.replace(/\b[A-Za-z_]\w*(?=\s*[.(])/g, ' ');
|
|
7343
|
+
return (s.match(/\b[a-z_]\w*\b/g) ?? []).some(id => id !== name && !JAVA_OPERAND_KEYWORDS.has(id));
|
|
7344
|
+
}
|
|
7345
|
+
function findJavaLiteralAllowlistGuardSanitizers(code) {
|
|
7346
|
+
const sanitizers = [];
|
|
7347
|
+
const lines = code.split('\n');
|
|
7348
|
+
const literalList = String.raw `(?:Set|List)\s*\.\s*of|Arrays\s*\.\s*asList`;
|
|
7349
|
+
const literalArgs = String.raw `\(\s*"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*\s*\)`;
|
|
7350
|
+
// Constants declared from a literal list in this file.
|
|
7351
|
+
const constants = new Set();
|
|
7352
|
+
const constRe = new RegExp(String.raw `\bstatic\s+final\s+[\w<>,.\s]+?\s([A-Za-z_]\w*)\s*=\s*(?:${literalList})\s*${literalArgs}\s*;`);
|
|
7353
|
+
for (const l of lines) {
|
|
7354
|
+
const m = constRe.exec(l);
|
|
7355
|
+
if (m)
|
|
7356
|
+
constants.add(m[1]);
|
|
7357
|
+
}
|
|
7358
|
+
const guardRe = new RegExp(String.raw `\bif\s*\(\s*!\s*(?:(?:${literalList})\s*${literalArgs}|([A-Za-z_]\w*))\s*\.\s*contains\s*\(\s*([A-Za-z_]\w*)\s*\)\s*\)`);
|
|
7359
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
7360
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7361
|
+
const m = guardRe.exec(lines[i]);
|
|
7362
|
+
if (!m)
|
|
7363
|
+
continue;
|
|
7364
|
+
if (m[1] !== undefined && !constants.has(m[1]))
|
|
7365
|
+
continue;
|
|
7366
|
+
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
7367
|
+
continue;
|
|
7368
|
+
const name = m[2];
|
|
7369
|
+
const mention = new RegExp(String.raw `(?<![\w.])${name}\b`);
|
|
7370
|
+
const reassign = new RegExp(String.raw `^\s*(?:[\w.<>[\]]+\s+)?${name}\s*=(?!=)`);
|
|
7371
|
+
// Skip the guard's own block, then credit to the end of the enclosing method.
|
|
7372
|
+
let start = i + 1;
|
|
7373
|
+
if (/\{\s*$/.test(lines[i])) {
|
|
7374
|
+
for (let d = 0, j = i; j < lines.length; j++) {
|
|
7375
|
+
for (const ch of lines[j]) {
|
|
7376
|
+
if (ch === '{')
|
|
7377
|
+
d++;
|
|
7378
|
+
else if (ch === '}')
|
|
7379
|
+
d--;
|
|
7380
|
+
}
|
|
7381
|
+
if (d <= 0) {
|
|
7382
|
+
start = j + 1;
|
|
7383
|
+
break;
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
for (let j = start, d = 0; j < lines.length; j++) {
|
|
7388
|
+
for (const ch of lines[j]) {
|
|
7389
|
+
if (ch === '{')
|
|
7390
|
+
d++;
|
|
7391
|
+
else if (ch === '}')
|
|
7392
|
+
d--;
|
|
7393
|
+
}
|
|
7394
|
+
if (d < 0)
|
|
7395
|
+
break;
|
|
7396
|
+
if (reassign.test(lines[j]))
|
|
7397
|
+
break;
|
|
7398
|
+
if (!mention.test(lines[j]))
|
|
7399
|
+
continue;
|
|
7400
|
+
// The credit is line-keyed: a line that also carries another local
|
|
7401
|
+
// (`"... n='" + who + "' ORDER BY " + col`) would clean `who` too.
|
|
7402
|
+
if (otherLocalOperand(lines[j], name))
|
|
7403
|
+
continue;
|
|
7404
|
+
sanitizers.push({
|
|
7405
|
+
type: 'java_literal_allowlist_guard',
|
|
7406
|
+
method: 'guard',
|
|
7407
|
+
line: j + 1,
|
|
7408
|
+
sanitizes: ['trust_boundary', 'sql_injection', 'command_injection', 'path_traversal', 'ssrf',
|
|
7409
|
+
'ldap_injection', 'xpath_injection', 'log_injection', 'xss', 'open_redirect',
|
|
7410
|
+
'external_taint_escape'],
|
|
7411
|
+
});
|
|
7412
|
+
}
|
|
7413
|
+
}
|
|
7414
|
+
return sanitizers;
|
|
7415
|
+
}
|
|
5788
7416
|
/**
|
|
5789
7417
|
* cognium-dev#272 — a validating predicate followed by an EARLY RETURN is a
|
|
5790
7418
|
* guard, and the value reaching the sink afterwards is already validated.
|
|
@@ -6052,6 +7680,19 @@ function findJavaPathGetFileNameSanitizers(code) {
|
|
|
6052
7680
|
* strips CRLF. In Java that means an explicit `String`/`CharSequence` return
|
|
6053
7681
|
* type, which keeps `void` log-and-forget methods out.
|
|
6054
7682
|
*/
|
|
7683
|
+
/** True when the Java text strips both `\r` and `\n` via replace / replaceAll (cognium-dev#605). */
|
|
7684
|
+
function javaBodyStripsCrAndLf(body) {
|
|
7685
|
+
let cr = false;
|
|
7686
|
+
let lf = false;
|
|
7687
|
+
for (const m of body.matchAll(/\.\s*(replace|replaceAll)\s*\(\s*(?:"((?:\\.|[^"\\])*)"|'(\\.|[^'\\])')/g)) {
|
|
7688
|
+
const cov = javaReplaceCrLfCoverage(m[1], m[2] ?? m[3] ?? '');
|
|
7689
|
+
if (cov.cr)
|
|
7690
|
+
cr = true;
|
|
7691
|
+
if (cov.lf)
|
|
7692
|
+
lf = true;
|
|
7693
|
+
}
|
|
7694
|
+
return cr && lf;
|
|
7695
|
+
}
|
|
6055
7696
|
function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
6056
7697
|
const sanitizers = [];
|
|
6057
7698
|
const lines = code.split('\n');
|
|
@@ -6068,8 +7709,25 @@ function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
|
6068
7709
|
const m = declRe.exec(lines[i]);
|
|
6069
7710
|
if (!m)
|
|
6070
7711
|
continue;
|
|
6071
|
-
|
|
6072
|
-
|
|
7712
|
+
if (language !== 'java') {
|
|
7713
|
+
const body = lines.slice(i, Math.min(lines.length, i + bodyWindow)).join('\n');
|
|
7714
|
+
if (stripRe.test(body))
|
|
7715
|
+
wrappers.add(m[1]);
|
|
7716
|
+
continue;
|
|
7717
|
+
}
|
|
7718
|
+
// Java: stop at the next declaration so one helper's body does not
|
|
7719
|
+
// credit the helper above it, and require the strip to cover BOTH CR and
|
|
7720
|
+
// LF. An LF-only `replace("\n", "")` leaves CR, which still forges a
|
|
7721
|
+
// log line (cognium-dev#605).
|
|
7722
|
+
let end = Math.min(lines.length, i + bodyWindow);
|
|
7723
|
+
for (let j = i + 1; j < end; j++) {
|
|
7724
|
+
if (declRe.test(lines[j])) {
|
|
7725
|
+
end = j;
|
|
7726
|
+
break;
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
const body = lines.slice(i, end).join('\n');
|
|
7730
|
+
if (javaBodyStripsCrAndLf(body))
|
|
6073
7731
|
wrappers.add(m[1]);
|
|
6074
7732
|
}
|
|
6075
7733
|
if (wrappers.size === 0)
|
|
@@ -7160,8 +8818,13 @@ export function findJsPatternFindings(code, file) {
|
|
|
7160
8818
|
* the args is wrapped in a recognized HTML escaper
|
|
7161
8819
|
* (`html.EscapeString` / `template.HTMLEscapeString` /
|
|
7162
8820
|
* `template.HTMLEscaper`).
|
|
8821
|
+
*
|
|
8822
|
+
* A write whose every argument is a local holding an int or bool measure of
|
|
8823
|
+
* a value (`scalarVarsAt(line)`, #568 — `fmt.Fprintf(w, "%d", n)` with
|
|
8824
|
+
* `n := len(...)`) is skipped. Any other argument, including request reads
|
|
8825
|
+
* the taint engine has no source for (`r.URL.Path`), still reports.
|
|
7163
8826
|
*/
|
|
7164
|
-
export function findGoXssFindings(code, file) {
|
|
8827
|
+
export function findGoXssFindings(code, file, scalarVarsAt = () => new Set()) {
|
|
7165
8828
|
const out = [];
|
|
7166
8829
|
const lines = code.split('\n');
|
|
7167
8830
|
const rwNames = new Set();
|
|
@@ -7192,6 +8855,11 @@ export function findGoXssFindings(code, file) {
|
|
|
7192
8855
|
// Require at least one identifier in tail outside string literals.
|
|
7193
8856
|
if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, '')))
|
|
7194
8857
|
continue;
|
|
8858
|
+
const idents = tail.replace(/"(?:[^"\\]|\\.)*"|`[^`]*`/g, '')
|
|
8859
|
+
.match(/[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/g) ?? [];
|
|
8860
|
+
const scalars = scalarVarsAt(i + 1);
|
|
8861
|
+
if (idents.length > 0 && idents.every(id => scalars.has(id)))
|
|
8862
|
+
continue;
|
|
7195
8863
|
out.push({
|
|
7196
8864
|
id: `xss-${file}-${i + 1}`,
|
|
7197
8865
|
pass: 'language-sources',
|
|
@@ -10556,4 +12224,225 @@ export function findJsTemplateInjectionSstiFindings(code, file) {
|
|
|
10556
12224
|
}
|
|
10557
12225
|
return findings;
|
|
10558
12226
|
}
|
|
12227
|
+
// ---------------------------------------------------------------------------
|
|
12228
|
+
// cognium-dev#566 — Spring MVC return-as-body XSS (Java)
|
|
12229
|
+
// ---------------------------------------------------------------------------
|
|
12230
|
+
const SPRING_MAPPING_ANN = /^(?:Get|Post|Put|Delete|Patch|Request)Mapping\b/;
|
|
12231
|
+
const SPRING_WEB_PARAM_ANN = /^(?:RequestParam|PathVariable|RequestHeader|CookieValue|MatrixVariable)\b/;
|
|
12232
|
+
const SPRING_NON_TEXT_PARAM_TYPE = /^(?:int|long|short|byte|double|float|boolean|char|Integer|Long|Short|Byte|Double|Float|Boolean|Character|BigDecimal|BigInteger|UUID|LocalDate|LocalDateTime|LocalTime|Instant|OffsetDateTime|ZonedDateTime|Date)$/;
|
|
12233
|
+
/**
|
|
12234
|
+
* Value of `produces = …` on a `@*Mapping` annotation, or null when absent.
|
|
12235
|
+
* Only the produces value is considered — a path like `/widgets.html` must not
|
|
12236
|
+
* look like an HTML media type.
|
|
12237
|
+
*/
|
|
12238
|
+
function springProducesValue(annotations) {
|
|
12239
|
+
for (const a of annotations) {
|
|
12240
|
+
if (!/^\w+Mapping\b/.test(a) || !/\bproduces\s*=/.test(a))
|
|
12241
|
+
continue;
|
|
12242
|
+
const m = /\bproduces\s*=\s*(?:\{([^}]*)\}|([^,)]+))/.exec(a);
|
|
12243
|
+
if (!m)
|
|
12244
|
+
continue;
|
|
12245
|
+
return (m[1] ?? m[2] ?? '').trim();
|
|
12246
|
+
}
|
|
12247
|
+
return null;
|
|
12248
|
+
}
|
|
12249
|
+
/** True when a produces value names a non-HTML media type. */
|
|
12250
|
+
function springProducesValueIsNonHtml(value) {
|
|
12251
|
+
return !/html/i.test(value);
|
|
12252
|
+
}
|
|
12253
|
+
/** Markup or string concatenation around a value (the Python return-XSS gate, #368). */
|
|
12254
|
+
function javaExprLooksLikeHtml(expr) {
|
|
12255
|
+
return /"[^"]*<[^"]*"/.test(expr) || /"\s*\+/.test(expr) || /\+\s*"/.test(expr);
|
|
12256
|
+
}
|
|
12257
|
+
/** Parentheses outside string literals never close below depth 0 and end at 0. */
|
|
12258
|
+
function javaParensBalanced(expr) {
|
|
12259
|
+
let depth = 0;
|
|
12260
|
+
for (const ch of expr.replace(/"(?:[^"\\]|\\.)*"/g, '""')) {
|
|
12261
|
+
if (ch === '(')
|
|
12262
|
+
depth++;
|
|
12263
|
+
else if (ch === ')' && --depth < 0)
|
|
12264
|
+
return false;
|
|
12265
|
+
}
|
|
12266
|
+
return depth === 0;
|
|
12267
|
+
}
|
|
12268
|
+
/**
|
|
12269
|
+
* cognium-dev#566 — the value a Spring MVC handler returns is the response
|
|
12270
|
+
* body when the class is `@RestController` (or `@ResponseBody` sits on the
|
|
12271
|
+
* class or the method), and always when the handler returns a
|
|
12272
|
+
* `ResponseEntity`. A `String` body is written with the negotiated content
|
|
12273
|
+
* type, so a browser request gets it as `text/html`: request data
|
|
12274
|
+
* concatenated into markup there is reflected XSS.
|
|
12275
|
+
*
|
|
12276
|
+
* Positive evidence only:
|
|
12277
|
+
* - the method carries a `@*Mapping` annotation;
|
|
12278
|
+
* - it returns `String` under a body-writing controller, or
|
|
12279
|
+
* `ResponseEntity<String>` via `ResponseEntity.ok(x)` /
|
|
12280
|
+
* `new ResponseEntity<>(x, …)`, or `ResponseEntity.ok().body(x)` /
|
|
12281
|
+
* `ResponseEntity.status(...).body(x)` on a body-writing controller);
|
|
12282
|
+
* - the returned expression references a request-bound parameter
|
|
12283
|
+
* (`@RequestParam`, `@PathVariable`, `@RequestHeader`, `@CookieValue`,
|
|
12284
|
+
* `@MatrixVariable`, a `String` `@RequestBody`) or an in-method source,
|
|
12285
|
+
* directly or through a local assigned from it, and looks like HTML
|
|
12286
|
+
* (markup or string concatenation, directly or in that local's assignment).
|
|
12287
|
+
*
|
|
12288
|
+
* Not a sink: a plain `@Controller` returning a `String` (a view name),
|
|
12289
|
+
* `produces = ` a non-HTML media type on the method or class mapping, a
|
|
12290
|
+
* non-`String` return (serialised as JSON), a returned value that does not
|
|
12291
|
+
* look like markup, and any handler with an xss-covering sanitizer between
|
|
12292
|
+
* its start and the return.
|
|
12293
|
+
*/
|
|
12294
|
+
export function findJavaSpringReturnXSSSinks(code, types, sources, xssSanitizerLines) {
|
|
12295
|
+
const out = [];
|
|
12296
|
+
const lines = code.split('\n');
|
|
12297
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12298
|
+
const stripStrings = (s) => s.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
12299
|
+
const refers = (expr, name) => new RegExp(`(?<![\\w.])${esc(name)}\\b`).test(stripStrings(expr));
|
|
12300
|
+
for (const t of types) {
|
|
12301
|
+
const ann = t.annotations ?? [];
|
|
12302
|
+
const isRest = ann.some(a => /^RestController\b/.test(a));
|
|
12303
|
+
if (!isRest && !ann.some(a => /^Controller\b/.test(a)))
|
|
12304
|
+
continue;
|
|
12305
|
+
const classProduces = springProducesValue(ann);
|
|
12306
|
+
const classBody = isRest || ann.some(a => /^ResponseBody\b/.test(a));
|
|
12307
|
+
for (const m of t.methods) {
|
|
12308
|
+
if (!m.annotations.some(a => SPRING_MAPPING_ANN.test(a)))
|
|
12309
|
+
continue;
|
|
12310
|
+
// Method-level produces overrides class-level; only non-HTML suppresses.
|
|
12311
|
+
const methodProduces = springProducesValue(m.annotations);
|
|
12312
|
+
const produces = methodProduces ?? classProduces;
|
|
12313
|
+
if (produces != null && springProducesValueIsNonHtml(produces))
|
|
12314
|
+
continue;
|
|
12315
|
+
const rt = (m.return_type ?? '').replace(/\s+/g, '');
|
|
12316
|
+
const entity = rt === 'ResponseEntity<String>';
|
|
12317
|
+
const body = rt === 'String' && (classBody || m.annotations.some(a => /^ResponseBody\b/.test(a)));
|
|
12318
|
+
if (!entity && !body)
|
|
12319
|
+
continue;
|
|
12320
|
+
// Seeds: request-bound parameters, then in-method sources with a variable.
|
|
12321
|
+
const seeds = new Map();
|
|
12322
|
+
for (const p of m.parameters) {
|
|
12323
|
+
// Spring converts to the declared type first: a number, boolean,
|
|
12324
|
+
// UUID or date cannot carry markup.
|
|
12325
|
+
if (SPRING_NON_TEXT_PARAM_TYPE.test((p.type ?? '').replace(/^(?:[\w]+\.)+/, '')))
|
|
12326
|
+
continue;
|
|
12327
|
+
const bound = p.annotations.some(a => SPRING_WEB_PARAM_ANN.test(a))
|
|
12328
|
+
|| (p.annotations.some(a => /^RequestBody\b/.test(a)) && p.type === 'String');
|
|
12329
|
+
if (bound)
|
|
12330
|
+
seeds.set(p.name, p.line ?? m.start_line);
|
|
12331
|
+
}
|
|
12332
|
+
for (const s of sources) {
|
|
12333
|
+
// A plain parameter of the handler is not request data by itself.
|
|
12334
|
+
if (s.type === 'interprocedural_param')
|
|
12335
|
+
continue;
|
|
12336
|
+
if (!s.variable || !/^[A-Za-z_]\w*$/.test(s.variable))
|
|
12337
|
+
continue;
|
|
12338
|
+
if (s.line > m.start_line && s.line <= m.end_line && !seeds.has(s.variable))
|
|
12339
|
+
seeds.set(s.variable, s.line);
|
|
12340
|
+
}
|
|
12341
|
+
if (seeds.size === 0)
|
|
12342
|
+
continue;
|
|
12343
|
+
// Locals assigned from a seed (method body only), each tied to its seed's line.
|
|
12344
|
+
const bodyText = lines.slice(m.start_line - 1, m.end_line).join('\n');
|
|
12345
|
+
const carriers = new Map();
|
|
12346
|
+
for (const [n, l] of seeds)
|
|
12347
|
+
carriers.set(n, { sourceLine: l, assignLine: l });
|
|
12348
|
+
const derived = [...buildJavaTaintedVars(bodyText, new Set(seeds.keys()))].sort((a, b) => a[1] - b[1]);
|
|
12349
|
+
for (const [n, rel] of derived) {
|
|
12350
|
+
if (carriers.has(n))
|
|
12351
|
+
continue;
|
|
12352
|
+
const assignLine = m.start_line + rel - 1;
|
|
12353
|
+
const from = [...carriers].find(([c]) => refers(lines[assignLine - 1] ?? '', c));
|
|
12354
|
+
carriers.set(n, { sourceLine: from ? from[1].sourceLine : m.start_line, assignLine });
|
|
12355
|
+
}
|
|
12356
|
+
for (let i = m.start_line - 1; i < Math.min(m.end_line, lines.length); i++) {
|
|
12357
|
+
if (!/^\s*return\b/.test(lines[i] ?? ''))
|
|
12358
|
+
continue;
|
|
12359
|
+
let stmt = lines[i];
|
|
12360
|
+
for (let j = i + 1; !stmt.includes(';') && j < Math.min(m.end_line, lines.length); j++)
|
|
12361
|
+
stmt += ' ' + lines[j];
|
|
12362
|
+
stmt = stmt.replace(/^\s*return\b/, '').replace(/;[^;]*$/, '');
|
|
12363
|
+
const sinkLine = i + 1;
|
|
12364
|
+
let expr = stmt;
|
|
12365
|
+
if (entity) {
|
|
12366
|
+
// `ResponseEntity.ok(x)` / `new ResponseEntity<>(x, status)` /
|
|
12367
|
+
// `ResponseEntity.ok().body(x)` / `ResponseEntity.status(...).body(x)`.
|
|
12368
|
+
const em = /^\s*ResponseEntity\s*\.\s*ok\s*\(\s*\)\s*\.\s*body\s*\(([\s\S]+)\)\s*$/.exec(stmt)
|
|
12369
|
+
?? /^\s*ResponseEntity\s*\.\s*status\s*\(([\s\S]+)\)\s*\.\s*body\s*\(([\s\S]+)\)\s*$/.exec(stmt)
|
|
12370
|
+
?? /^\s*ResponseEntity\s*\.\s*ok\s*\(([\s\S]+)\)\s*$/.exec(stmt)
|
|
12371
|
+
?? /^\s*new\s+ResponseEntity\s*(?:<[^>]*>)?\s*\(([\s\S]+),[^,()]*\)\s*$/.exec(stmt);
|
|
12372
|
+
if (!em)
|
|
12373
|
+
continue;
|
|
12374
|
+
const payload = em.length === 3 ? em[2] : em[1];
|
|
12375
|
+
if (!payload || !javaParensBalanced(payload))
|
|
12376
|
+
continue;
|
|
12377
|
+
if (em.length === 3 && !javaParensBalanced(em[1]))
|
|
12378
|
+
continue;
|
|
12379
|
+
expr = payload;
|
|
12380
|
+
}
|
|
12381
|
+
const hit = [...carriers].find(([c, info]) => info.assignLine <= sinkLine && refers(expr, c));
|
|
12382
|
+
if (!hit)
|
|
12383
|
+
continue;
|
|
12384
|
+
const [name, info] = hit;
|
|
12385
|
+
let htmlish = javaExprLooksLikeHtml(expr);
|
|
12386
|
+
if (!htmlish && new RegExp(`^\\s*${esc(name)}\\s*$`).test(expr) && info.assignLine !== info.sourceLine) {
|
|
12387
|
+
htmlish = javaExprLooksLikeHtml(lines[info.assignLine - 1] ?? '');
|
|
12388
|
+
}
|
|
12389
|
+
if (!htmlish)
|
|
12390
|
+
continue;
|
|
12391
|
+
// Suppress only when an xss sanitizer's operand is the returned carrier
|
|
12392
|
+
// (or the carrier local was assigned from that sanitizer). Escaping a
|
|
12393
|
+
// different variable must not clear a return of another carrier.
|
|
12394
|
+
const xssEscapeOpRe = /\b(?:Encode\.(?:forHtml|forHtmlAttribute|forHtmlContent|forJavaScript)|StringEscapeUtils\.escape(?:Html3|Html4|EcmaScript|Xml)|HtmlUtils\.htmlEscape(?:Decimal|Hex)?|Escaper\.escapeHtml|HtmlEscapers\.(?:escapeHtml|htmlEscaper)|Encoder\.encodeForHTML(?:Attribute)?|Jsoup\.clean)\s*\(\s*([A-Za-z_]\w*)/;
|
|
12395
|
+
const sanitizerCoversCarrier = xssSanitizerLines.some(l => {
|
|
12396
|
+
if (l < m.start_line || l > sinkLine)
|
|
12397
|
+
return false;
|
|
12398
|
+
const line = lines[l - 1] ?? '';
|
|
12399
|
+
const ops = [];
|
|
12400
|
+
let sm;
|
|
12401
|
+
const re = new RegExp(xssEscapeOpRe.source, 'g');
|
|
12402
|
+
while ((sm = re.exec(line)) !== null)
|
|
12403
|
+
ops.push(sm[1]);
|
|
12404
|
+
// Unparsed shapes (e.g. static-import `htmlEscape(title)`) do not suppress.
|
|
12405
|
+
if (ops.length === 0)
|
|
12406
|
+
return false;
|
|
12407
|
+
// Operand is the returned carrier.
|
|
12408
|
+
if (ops.some(op => op === name))
|
|
12409
|
+
return true;
|
|
12410
|
+
// Carrier assigned from the sanitizer call itself (`body = HtmlUtils.htmlEscape(x)`),
|
|
12411
|
+
// not a concat that merely mentions a sanitizer on the assignment line.
|
|
12412
|
+
if (l === info.assignLine) {
|
|
12413
|
+
const stripped = line.replace(/"(?:[^"\\]|\\.)*"/g, '""').trim();
|
|
12414
|
+
const am = new RegExp(`\\b${esc(name)}\\s*=\\s*(.+)$`).exec(stripped);
|
|
12415
|
+
if (!am)
|
|
12416
|
+
return false;
|
|
12417
|
+
const rhs = am[1].replace(/;\s*$/, '').trim();
|
|
12418
|
+
const call = xssEscapeOpRe.exec(rhs);
|
|
12419
|
+
if (!call || call.index !== 0)
|
|
12420
|
+
return false;
|
|
12421
|
+
// Whole RHS is one sanitizer invocation (no concatenation outside the call).
|
|
12422
|
+
let depth = 0;
|
|
12423
|
+
let ended = -1;
|
|
12424
|
+
for (let k = call.index; k < rhs.length; k++) {
|
|
12425
|
+
const ch = rhs[k];
|
|
12426
|
+
if (ch === '(')
|
|
12427
|
+
depth++;
|
|
12428
|
+
else if (ch === ')') {
|
|
12429
|
+
depth--;
|
|
12430
|
+
if (depth === 0) {
|
|
12431
|
+
ended = k;
|
|
12432
|
+
break;
|
|
12433
|
+
}
|
|
12434
|
+
}
|
|
12435
|
+
}
|
|
12436
|
+
return ended >= 0 && rhs.slice(ended + 1).trim() === '';
|
|
12437
|
+
}
|
|
12438
|
+
return false;
|
|
12439
|
+
});
|
|
12440
|
+
if (sanitizerCoversCarrier)
|
|
12441
|
+
continue;
|
|
12442
|
+
out.push({ sinkLine, sourceLine: info.sourceLine });
|
|
12443
|
+
}
|
|
12444
|
+
}
|
|
12445
|
+
}
|
|
12446
|
+
return out;
|
|
12447
|
+
}
|
|
10559
12448
|
//# sourceMappingURL=language-sources-pass.js.map
|