circle-ir 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +134 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +98 -3
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +109 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1176 -119
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +307 -31
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1349 -58
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +8 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -1
- package/dist/analysis/scalar-derivation.js +172 -0
- package/dist/analysis/scalar-derivation.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +531 -31
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +234 -13
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -3
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +24319 -19934
- package/dist/core/circle-ir-core.cjs +4936 -3575
- package/dist/core/circle-ir-core.js +4936 -3575
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +102 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +102 -43
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ import { attachSourceLineCode } from '../taint-matcher.js';
|
|
|
18
18
|
import { javaReplaceCrLfCoverage } from '../java-crlf-strip.js';
|
|
19
19
|
import { javaCallArgument, javaCanonicalBaseLacksSeparator } from '../java-canonical-prefix.js';
|
|
20
20
|
import { findGoStringsReplacerSanitizers } from '../go-strings-replacer.js';
|
|
21
|
+
import { findRustLiteralEqualityGuardSanitizers } from '../rust-literal-guard.js';
|
|
21
22
|
import { constLookupTables, isConstTableLookup } from '../const-table-lookup.js';
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Pattern tables (moved from analyzer.ts)
|
|
@@ -222,6 +223,22 @@ export class LanguageSourcesPass {
|
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
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
|
+
}
|
|
225
242
|
// -- Python: assignment sources, trust-boundary sinks, return-XSS sinks --
|
|
226
243
|
additionalSources.push(...findPythonAssignmentSources(code, language));
|
|
227
244
|
const pyTaintedVars = language === 'python' ? buildPythonTaintedVars(code) : new Map();
|
|
@@ -342,6 +359,7 @@ export class LanguageSourcesPass {
|
|
|
342
359
|
// recognition.
|
|
343
360
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
344
361
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
362
|
+
additionalSanitizers.push(...findPythonLiteralMembershipGuardSanitizers(code));
|
|
345
363
|
additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
|
|
346
364
|
additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
|
|
347
365
|
additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
|
|
@@ -401,6 +419,8 @@ export class LanguageSourcesPass {
|
|
|
401
419
|
if (language === 'rust') {
|
|
402
420
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
403
421
|
additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
|
|
422
|
+
// cognium-dev#293: `if x != "a" && x != "b" { return Err(..) }` literal-equality guard.
|
|
423
|
+
additionalSanitizers.push(...findRustLiteralEqualityGuardSanitizers(code));
|
|
404
424
|
// Sprint 77a (#216 Pattern X): argv-form Command::new(literal).arg(...)
|
|
405
425
|
// sanitizer.
|
|
406
426
|
additionalSanitizers.push(...findRustArgvCommandSanitizers(code));
|
|
@@ -544,8 +564,6 @@ export class LanguageSourcesPass {
|
|
|
544
564
|
const tm = ctx.getResult('taint-matcher');
|
|
545
565
|
const xssSan = tm.sanitizers.filter(sa => sa.sanitizes.includes('xss')).map(sa => sa.line);
|
|
546
566
|
for (const r of findJavaSpringReturnXSSSinks(code, types, tm.sources, xssSan)) {
|
|
547
|
-
if (additionalSinks.some(s => s.line === r.sinkLine && s.type === 'xss'))
|
|
548
|
-
continue;
|
|
549
567
|
additionalSinks.push({
|
|
550
568
|
type: 'xss',
|
|
551
569
|
cwe: 'CWE-79',
|
|
@@ -1610,6 +1628,50 @@ function findGoRequestBodySources(sourceCode, language) {
|
|
|
1610
1628
|
}
|
|
1611
1629
|
return sources;
|
|
1612
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
|
+
}
|
|
1613
1675
|
function findCSharpRequestSources(sourceCode, language) {
|
|
1614
1676
|
if (language !== 'csharp')
|
|
1615
1677
|
return [];
|
|
@@ -1632,6 +1694,13 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
1632
1694
|
// of defect as #350 (Java try-with-resources): a resource-declaration syntax
|
|
1633
1695
|
// the scanner never looked at.
|
|
1634
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*$/;
|
|
1635
1704
|
// `Request.*` is the inherited controller member. A parameter typed
|
|
1636
1705
|
// `HttpRequest` / `HttpRequestBase` is the same surface under whatever
|
|
1637
1706
|
// name the method uses (`req` in Juliet) — cognium-dev#501.
|
|
@@ -1761,97 +1830,136 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
1761
1830
|
// when it is not the line's code.
|
|
1762
1831
|
const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
|
|
1763
1832
|
for (let i = 0; i < lines.length; i++) {
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
// it captures `sr` with EVERYTHING after `=` as the right-hand side. That
|
|
1771
|
-
// RHS contains `sr.ReadLine()`, so the line was classified as a read and
|
|
1772
|
-
// bound to `sr` — the reader, not `data`. The embedded-assignment path
|
|
1773
|
-
// handles exactly this shape and finds both `sr` and `data`, but it only
|
|
1774
|
-
// ran when `assignRe` failed to match at all. Prefer it whenever the
|
|
1775
|
-
// captured RHS crosses a block boundary. (#504 was the mirror of this:
|
|
1776
|
-
// there `assignRe` did not match and the inner assignment was lost.)
|
|
1777
|
-
const rhsCrossesBlock = m !== null && m[2].includes('{');
|
|
1778
|
-
if (!m || rhsCrossesBlock) {
|
|
1779
|
-
const stmt = lines[i];
|
|
1780
|
-
if (stmt.includes('{') && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
|
|
1781
|
-
const emb = new RegExp(embeddedAssignRe.source, 'g');
|
|
1782
|
-
let em;
|
|
1783
|
-
let seeded = false;
|
|
1784
|
-
while ((em = emb.exec(stmt)) !== null) {
|
|
1785
|
-
const varName = em[1];
|
|
1786
|
-
const rhs = em[2];
|
|
1787
|
-
const type = classify(rhs);
|
|
1788
|
-
if (!type)
|
|
1789
|
-
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) {
|
|
1790
1839
|
const lineNumber = i + 1;
|
|
1791
|
-
if (sources.some(s => s.line === lineNumber && s.variable ===
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
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;
|
|
1803
1851
|
}
|
|
1804
|
-
|
|
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
|
+
}
|
|
1805
1869
|
continue;
|
|
1870
|
+
}
|
|
1806
1871
|
}
|
|
1807
|
-
|
|
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
|
+
});
|
|
1808
1944
|
continue;
|
|
1809
|
-
|
|
1810
|
-
|
|
1945
|
+
}
|
|
1946
|
+
const [, varName, rhs] = m;
|
|
1947
|
+
const type = classify(rhs);
|
|
1948
|
+
if (!type)
|
|
1811
1949
|
continue;
|
|
1812
|
-
const { type, read } = hit;
|
|
1813
1950
|
const lineNumber = i + 1;
|
|
1814
|
-
if (sources.some(s => s.line === lineNumber))
|
|
1815
|
-
continue;
|
|
1816
|
-
// `variable` is the `<inline>` marker (the same one the inline
|
|
1817
|
-
// colocation flow puts on its path steps), never a real name. It
|
|
1818
|
-
// must be set, and must match nothing:
|
|
1819
|
-
// - with NO variable, `findInitialTaint` seeds every def on the
|
|
1820
|
-
// FOLLOWING line by adjacency, so `Log(file.FileName);` followed by
|
|
1821
|
-
// `var x = "/tmp/a";` tainted the constant `x`;
|
|
1822
|
-
// - with the read text itself (`file.FileName`) as the variable, the
|
|
1823
|
-
// colocation path treats it as an LHS binding and hands the pair
|
|
1824
|
-
// to the variable scan, which only looks at strictly later sinks —
|
|
1825
|
-
// so the same-line flow this exists for was lost.
|
|
1826
|
-
// The marker reaches only a sink on this very statement.
|
|
1827
|
-
const code = stmt.trim();
|
|
1951
|
+
if (sources.some(s => s.line === lineNumber && s.variable === varName))
|
|
1952
|
+
continue;
|
|
1828
1953
|
sources.push({
|
|
1829
1954
|
type,
|
|
1830
|
-
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.
|
|
1831
1957
|
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1832
1958
|
line: lineNumber,
|
|
1833
1959
|
confidence: 1.0,
|
|
1834
|
-
variable:
|
|
1835
|
-
code,
|
|
1960
|
+
variable: varName,
|
|
1836
1961
|
});
|
|
1837
|
-
continue;
|
|
1838
1962
|
}
|
|
1839
|
-
const [, varName, rhs] = m;
|
|
1840
|
-
const type = classify(rhs);
|
|
1841
|
-
if (!type)
|
|
1842
|
-
continue;
|
|
1843
|
-
const lineNumber = i + 1;
|
|
1844
|
-
if (sources.some(s => s.line === lineNumber && s.variable === varName))
|
|
1845
|
-
continue;
|
|
1846
|
-
sources.push({
|
|
1847
|
-
type,
|
|
1848
|
-
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? '...' : ''}`,
|
|
1849
|
-
// #502 — Java parity: a database row read is medium, I/O is high.
|
|
1850
|
-
severity: type === 'db_input' ? 'medium' : 'high',
|
|
1851
|
-
line: lineNumber,
|
|
1852
|
-
confidence: 1.0,
|
|
1853
|
-
variable: varName,
|
|
1854
|
-
});
|
|
1855
1963
|
}
|
|
1856
1964
|
return sources;
|
|
1857
1965
|
}
|
|
@@ -2003,29 +2111,52 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
2003
2111
|
// #667: `REGISTRY[req.query.mod]` on a constant table yields a value the
|
|
2004
2112
|
// program wrote, not request data.
|
|
2005
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 = /^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|[^;}"'`])*/;
|
|
2006
2126
|
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
2007
2127
|
const line = lines[lineNum];
|
|
2008
2128
|
const lineNumber = lineNum + 1;
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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;
|
|
2027
2159
|
}
|
|
2028
|
-
break;
|
|
2029
2160
|
}
|
|
2030
2161
|
}
|
|
2031
2162
|
}
|
|
@@ -2041,8 +2172,8 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
2041
2172
|
// event-name allowlist to avoid firing on unrelated `.on('drain', cb)`
|
|
2042
2173
|
// / `.on('close', cb)` etc. that carry non-user-input.
|
|
2043
2174
|
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
2044
|
-
|
|
2045
|
-
|
|
2175
|
+
sources.push(...findExpressDestructuredRequestSources(sourceCode));
|
|
2176
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
2046
2177
|
sources.push(...findGraphqlResolverArgSources(sourceCode));
|
|
2047
2178
|
}
|
|
2048
2179
|
return sources;
|
|
@@ -2062,6 +2193,36 @@ function matchBrace(text, open) {
|
|
|
2062
2193
|
}
|
|
2063
2194
|
return -1;
|
|
2064
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
|
+
}
|
|
2065
2226
|
const EXPRESS_REQUEST_KEYS = {
|
|
2066
2227
|
query: 'http_param',
|
|
2067
2228
|
params: 'http_param',
|
|
@@ -2080,7 +2241,7 @@ const EXPRESS_REQUEST_KEYS = {
|
|
|
2080
2241
|
function findExpressDestructuredRequestSources(sourceCode) {
|
|
2081
2242
|
const sources = [];
|
|
2082
2243
|
const receivers = new Set();
|
|
2083
|
-
for (const m of sourceCode.matchAll(/\b(?:const|let|var)\s+(\w+)\s
|
|
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)) {
|
|
2084
2245
|
receivers.add(m[1]);
|
|
2085
2246
|
}
|
|
2086
2247
|
if (receivers.size === 0)
|
|
@@ -2095,7 +2256,8 @@ function findExpressDestructuredRequestSources(sourceCode) {
|
|
|
2095
2256
|
continue;
|
|
2096
2257
|
// The pattern must be followed by another parameter or `)`, i.e. it is
|
|
2097
2258
|
// the handler's first parameter.
|
|
2098
|
-
|
|
2259
|
+
// A TypeScript `: Request` annotation after the pattern is skipped first.
|
|
2260
|
+
if (!/^\s*[,)]/.test(sourceCode.slice(skipTypeAnnotation(sourceCode, close))))
|
|
2099
2261
|
continue;
|
|
2100
2262
|
const line = sourceCode.slice(0, m.index + m[0].length).split('\n').length;
|
|
2101
2263
|
const inner = sourceCode.slice(open + 1, close - 1);
|
|
@@ -2140,11 +2302,127 @@ function findExpressDestructuredRequestSources(sourceCode) {
|
|
|
2140
2302
|
}
|
|
2141
2303
|
return sources;
|
|
2142
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
|
+
}
|
|
2143
2421
|
/**
|
|
2144
2422
|
* cognium-dev#586 — GraphQL resolver-map arguments. The second parameter of a
|
|
2145
2423
|
* function under `Query` / `Mutation` / `Subscription` in the object passed as
|
|
2146
2424
|
* `resolvers` to `new ApolloServer({…})` / `makeExecutableSchema({…})` (inline,
|
|
2147
|
-
* or a same-file `const resolvers = {…}
|
|
2425
|
+
* or a same-file `const resolvers = {…}`, optionally type-annotated) is client-supplied. Parameter 0
|
|
2148
2426
|
* (`parent`) and 2 (`context`) are never seeded.
|
|
2149
2427
|
*/
|
|
2150
2428
|
function findGraphqlResolverArgSources(sourceCode) {
|
|
@@ -2166,11 +2444,17 @@ function findGraphqlResolverArgSources(sourceCode) {
|
|
|
2166
2444
|
if (!named)
|
|
2167
2445
|
continue;
|
|
2168
2446
|
const ident = named[1] ?? 'resolvers';
|
|
2169
|
-
|
|
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);
|
|
2170
2451
|
if (decl)
|
|
2171
2452
|
maps.push(decl.index + decl[0].length - 1);
|
|
2172
2453
|
}
|
|
2173
|
-
|
|
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;
|
|
2174
2458
|
for (const open of maps) {
|
|
2175
2459
|
const close = matchBrace(sourceCode, open);
|
|
2176
2460
|
if (close < 0)
|
|
@@ -2193,18 +2477,46 @@ function findGraphqlResolverArgSources(sourceCode) {
|
|
|
2193
2477
|
else if (ch === '}' || ch === ')')
|
|
2194
2478
|
depth--;
|
|
2195
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();
|
|
2196
2485
|
for (const f of typeBody.matchAll(FN_RE)) {
|
|
2197
2486
|
const at = f.index + f[0].indexOf(f[1]);
|
|
2198
2487
|
if (depthAt[at] !== 0)
|
|
2199
2488
|
continue;
|
|
2200
|
-
const
|
|
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;
|
|
2201
2513
|
sources.push({
|
|
2202
2514
|
type: 'http_param',
|
|
2203
|
-
location: `GraphQL ${t[1]}.${
|
|
2515
|
+
location: `GraphQL ${t[1]}.${fn} resolver args '${b.name}' at line ${b.line}`,
|
|
2204
2516
|
severity: 'high',
|
|
2205
|
-
line,
|
|
2517
|
+
line: b.line,
|
|
2206
2518
|
confidence: 0.9,
|
|
2207
|
-
variable:
|
|
2519
|
+
variable: b.name,
|
|
2208
2520
|
});
|
|
2209
2521
|
}
|
|
2210
2522
|
}
|
|
@@ -2832,6 +3144,480 @@ function isSafeJinjaReturnExpr(expr) {
|
|
|
2832
3144
|
const trimmed = expr.trim();
|
|
2833
3145
|
return JINJA_SAFE_RTS_RE.test(trimmed) || JINJA_SAFE_TEMPLATE_RE.test(trimmed);
|
|
2834
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
|
+
}
|
|
2835
3621
|
export function findPythonReturnXSSSinks(sourceCode, taintedVars) {
|
|
2836
3622
|
if (taintedVars.size === 0)
|
|
2837
3623
|
return [];
|
|
@@ -3099,6 +3885,43 @@ export function buildRustTaintedVars(sourceCode, seedVars) {
|
|
|
3099
3885
|
}
|
|
3100
3886
|
return derived;
|
|
3101
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
|
+
}
|
|
3102
3925
|
/**
|
|
3103
3926
|
* cognium-dev #220 — Java derived-taint expansion.
|
|
3104
3927
|
*
|
|
@@ -3163,8 +3986,46 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
|
3163
3986
|
const trimmed = line.trimStart();
|
|
3164
3987
|
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
3165
3988
|
continue;
|
|
3166
|
-
|
|
3167
|
-
|
|
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
|
+
}
|
|
3168
4029
|
const m = declMatch ?? assignMatch;
|
|
3169
4030
|
if (!m)
|
|
3170
4031
|
continue;
|
|
@@ -4517,6 +5378,196 @@ function findPythonRegexAllowlistWrapperSanitizers(code) {
|
|
|
4517
5378
|
}
|
|
4518
5379
|
return sanitizers;
|
|
4519
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;
|
|
5532
|
+
}
|
|
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;
|
|
5549
|
+
sanitizers.push({
|
|
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
|
+
],
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
return sanitizers;
|
|
5570
|
+
}
|
|
4520
5571
|
/**
|
|
4521
5572
|
* Detect Python set-membership allow-list guards that should sanitize
|
|
4522
5573
|
* `xss` flows (cognium-dev #216 Sprint 74 Pattern B, SSTI fixture).
|
|
@@ -11226,8 +12277,8 @@ function javaParensBalanced(expr) {
|
|
|
11226
12277
|
* - the method carries a `@*Mapping` annotation;
|
|
11227
12278
|
* - it returns `String` under a body-writing controller, or
|
|
11228
12279
|
* `ResponseEntity<String>` via `ResponseEntity.ok(x)` /
|
|
11229
|
-
* `new ResponseEntity<>(x, …)`
|
|
11230
|
-
*
|
|
12280
|
+
* `new ResponseEntity<>(x, …)`, or `ResponseEntity.ok().body(x)` /
|
|
12281
|
+
* `ResponseEntity.status(...).body(x)` on a body-writing controller);
|
|
11231
12282
|
* - the returned expression references a request-bound parameter
|
|
11232
12283
|
* (`@RequestParam`, `@PathVariable`, `@RequestHeader`, `@CookieValue`,
|
|
11233
12284
|
* `@MatrixVariable`, a `String` `@RequestBody`) or an in-method source,
|
|
@@ -11312,14 +12363,20 @@ export function findJavaSpringReturnXSSSinks(code, types, sources, xssSanitizerL
|
|
|
11312
12363
|
const sinkLine = i + 1;
|
|
11313
12364
|
let expr = stmt;
|
|
11314
12365
|
if (entity) {
|
|
11315
|
-
// `ResponseEntity.ok(x)` / `new ResponseEntity<>(x, status)`
|
|
11316
|
-
//
|
|
11317
|
-
|
|
11318
|
-
|
|
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)
|
|
11319
12371
|
?? /^\s*new\s+ResponseEntity\s*(?:<[^>]*>)?\s*\(([\s\S]+),[^,()]*\)\s*$/.exec(stmt);
|
|
11320
|
-
if (!em
|
|
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]))
|
|
11321
12378
|
continue;
|
|
11322
|
-
expr =
|
|
12379
|
+
expr = payload;
|
|
11323
12380
|
}
|
|
11324
12381
|
const hit = [...carriers].find(([c, info]) => info.assignLine <= sinkLine && refers(expr, c));
|
|
11325
12382
|
if (!hit)
|