circle-ir 4.14.1 → 4.15.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/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +9 -2
- 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/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +15 -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 +53 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- 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/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.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +11 -4
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +30 -15
- package/dist/analysis/passes/insecure-cookie-pass.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 +31 -3
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +894 -10
- 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 +162 -6
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +24 -3
- 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 +255 -25
- package/dist/analysis/passes/sink-filter-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 +268 -5
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/scalar-derivation.d.ts +25 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -0
- package/dist/analysis/scalar-derivation.js +196 -0
- package/dist/analysis/scalar-derivation.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +172 -8
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +9 -0
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +260 -11
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +8 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +3403 -1225
- package/dist/core/circle-ir-core.cjs +598 -26
- package/dist/core/circle-ir-core.js +598 -26
- package/dist/core/extractors/calls.js +19 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +240 -6
- package/dist/core/extractors/dfg.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/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +19 -0
- package/dist/languages/plugins/go.js.map +1 -1
- package/dist/resolution/cross-file.d.ts +7 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +10 -1
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +3 -3
|
@@ -15,6 +15,10 @@
|
|
|
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 { constLookupTables, isConstTableLookup } from '../const-table-lookup.js';
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
// Pattern tables (moved from analyzer.ts)
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
@@ -203,6 +207,7 @@ export class LanguageSourcesPass {
|
|
|
203
207
|
additionalSources.push(...findGoMultipartUploadSources(code, language));
|
|
204
208
|
additionalSources.push(...findGoGrpcRequestSources(code, language));
|
|
205
209
|
additionalSources.push(...findGoRequestBodySources(code, language));
|
|
210
|
+
additionalSources.push(...findGoFormIndexSources(code, language));
|
|
206
211
|
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
207
212
|
for (const s of jsDOMSinks) {
|
|
208
213
|
const alreadyExists = additionalSinks.some(x => x.line === s.line && x.cwe === s.cwe);
|
|
@@ -284,13 +289,19 @@ export class LanguageSourcesPass {
|
|
|
284
289
|
additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
|
|
285
290
|
additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
|
|
286
291
|
additionalSanitizers.push(...findGoPathContainmentGuardSanitizers(code));
|
|
292
|
+
// #348 / #293: strings.NewReplacer CR/LF strip and < > entity escape.
|
|
293
|
+
additionalSanitizers.push(...findGoStringsReplacerSanitizers(code));
|
|
287
294
|
// Sprint 78 (#190): Go ECB-mode weak-crypto detection.
|
|
288
295
|
const goMisconfigFindings = findGoPatternFindings(code, graph.ir.meta.file);
|
|
289
296
|
for (const finding of goMisconfigFindings) {
|
|
290
297
|
ctx.addFinding(finding);
|
|
291
298
|
}
|
|
292
299
|
// Sprint 81 (#189): Go xss — fmt.Fprint(f|ln) to http.ResponseWriter.
|
|
293
|
-
|
|
300
|
+
// #568: an arg that is an int/bool measure local carries no payload.
|
|
301
|
+
const scalarVarsAt = (line) => new Set(graph.usesAtLine(line)
|
|
302
|
+
.filter(u => u.def_id != null && graph.taintBarrierDefIds.has(u.def_id))
|
|
303
|
+
.map(u => u.variable));
|
|
304
|
+
for (const finding of findGoXssFindings(code, graph.ir.meta.file, scalarVarsAt)) {
|
|
294
305
|
ctx.addFinding(finding);
|
|
295
306
|
}
|
|
296
307
|
// Sprint 82 (#189): Go open_redirect — Header().Set("Location", taint).
|
|
@@ -522,6 +533,28 @@ export class LanguageSourcesPass {
|
|
|
522
533
|
// cognium-dev#269: `!file.getCanonicalPath().startsWith(base)` reject
|
|
523
534
|
// guard (the java.io.File OWASP-recommended containment defence).
|
|
524
535
|
additionalSanitizers.push(...findJavaCanonicalPathStartsWithGuardSanitizers(code));
|
|
536
|
+
// cognium-dev#293: `if (!Set.of("a", "b").contains(key)) throw` allow-list guard.
|
|
537
|
+
additionalSanitizers.push(...findJavaLiteralAllowlistGuardSanitizers(code));
|
|
538
|
+
// cognium-dev#668: `!<literal set>.contains(cls)` reject guard before reflection.
|
|
539
|
+
additionalSanitizers.push(...findJavaReflectionAllowlistGuardSanitizers(code, types));
|
|
540
|
+
// cognium-dev#566: a Spring @RestController / @ResponseBody / ResponseEntity
|
|
541
|
+
// handler's returned String is the response body. TaintPropagationPass
|
|
542
|
+
// links each synthesised sink to its source.
|
|
543
|
+
{
|
|
544
|
+
const tm = ctx.getResult('taint-matcher');
|
|
545
|
+
const xssSan = tm.sanitizers.filter(sa => sa.sanitizes.includes('xss')).map(sa => sa.line);
|
|
546
|
+
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
|
+
additionalSinks.push({
|
|
550
|
+
type: 'xss',
|
|
551
|
+
cwe: 'CWE-79',
|
|
552
|
+
line: r.sinkLine,
|
|
553
|
+
location: `Spring handler returns HTML with request input at line ${r.sinkLine}`,
|
|
554
|
+
confidence: 0.9,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
525
558
|
additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
|
|
526
559
|
// cognium-dev#293: the same strip, but living in a user-defined helper.
|
|
527
560
|
additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, 'java'));
|
|
@@ -1397,6 +1430,57 @@ function findGoGrpcRequestSources(sourceCode, language) {
|
|
|
1397
1430
|
}
|
|
1398
1431
|
return sources;
|
|
1399
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* cognium-dev #492 — index reads of request form maps.
|
|
1435
|
+
*
|
|
1436
|
+
* `r.PostForm.Get` / `r.Form.Get` are calls and are registered in the Go
|
|
1437
|
+
* plugin. `r.Form[k][0]`, `r.PostForm[k][0]`, and `r.MultipartForm.Value[k]`
|
|
1438
|
+
* are index expressions, so they never become a call. Seed the LHS when the
|
|
1439
|
+
* receiver is a name declared as `*http.Request`. A local `url.Values` does
|
|
1440
|
+
* not match that declaration.
|
|
1441
|
+
*/
|
|
1442
|
+
function findGoFormIndexSources(sourceCode, language) {
|
|
1443
|
+
if (language !== 'go')
|
|
1444
|
+
return [];
|
|
1445
|
+
if (!/\.(?:PostForm|Form|MultipartForm)\b/.test(sourceCode))
|
|
1446
|
+
return [];
|
|
1447
|
+
const lines = sourceCode.split('\n');
|
|
1448
|
+
const reqNames = new Set();
|
|
1449
|
+
const paramRe = /([A-Za-z_]\w*)\s+\*http\.Request\b/g;
|
|
1450
|
+
for (const line of lines) {
|
|
1451
|
+
let m;
|
|
1452
|
+
paramRe.lastIndex = 0;
|
|
1453
|
+
while ((m = paramRe.exec(line)) !== null)
|
|
1454
|
+
reqNames.add(m[1]);
|
|
1455
|
+
}
|
|
1456
|
+
if (reqNames.size === 0)
|
|
1457
|
+
return [];
|
|
1458
|
+
const alt = [...reqNames].map((n) => n.replace(/\$/g, '\\$')).join('|');
|
|
1459
|
+
// Group 3 is PostForm|Form. MultipartForm.Value leaves it unset.
|
|
1460
|
+
const indexRe = new RegExp(`^\\s*(?:var\\s+)?([A-Za-z_]\\w*)\\s*(?:,\\s*\\w+\\s*)?:?=\\s*(?:${alt})\\s*\\.\\s*(?:(PostForm|Form)|MultipartForm\\s*\\.\\s*Value)\\s*\\[`);
|
|
1461
|
+
const sources = [];
|
|
1462
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1463
|
+
const line = lines[i];
|
|
1464
|
+
if (/^\s*\/\//.test(line))
|
|
1465
|
+
continue;
|
|
1466
|
+
const m = indexRe.exec(line);
|
|
1467
|
+
if (!m)
|
|
1468
|
+
continue;
|
|
1469
|
+
const variable = m[1];
|
|
1470
|
+
if (variable === '_' || variable === '')
|
|
1471
|
+
continue;
|
|
1472
|
+
const kind = m[2];
|
|
1473
|
+
sources.push({
|
|
1474
|
+
type: kind === 'Form' ? 'http_param' : 'http_body',
|
|
1475
|
+
location: 'HTTP request form index',
|
|
1476
|
+
severity: 'high',
|
|
1477
|
+
line: i + 1,
|
|
1478
|
+
confidence: 0.9,
|
|
1479
|
+
variable,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
return sources;
|
|
1483
|
+
}
|
|
1400
1484
|
function findGoRequestBodySources(sourceCode, language) {
|
|
1401
1485
|
if (language !== 'go')
|
|
1402
1486
|
return [];
|
|
@@ -1916,6 +2000,9 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
1916
2000
|
const patterns = isRouteHandler
|
|
1917
2001
|
? [...JS_TAINTED_PATTERNS, { pattern: /\bparams\s*[.[]/, type: 'http_path' }]
|
|
1918
2002
|
: JS_TAINTED_PATTERNS;
|
|
2003
|
+
// #667: `REGISTRY[req.query.mod]` on a constant table yields a value the
|
|
2004
|
+
// program wrote, not request data.
|
|
2005
|
+
const lookupTables = constLookupTables(sourceCode, language);
|
|
1919
2006
|
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
1920
2007
|
const line = lines[lineNum];
|
|
1921
2008
|
const lineNumber = lineNum + 1;
|
|
@@ -1923,6 +2010,8 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
1923
2010
|
if (!assignmentMatch)
|
|
1924
2011
|
continue;
|
|
1925
2012
|
const [, varName, rhs] = assignmentMatch;
|
|
2013
|
+
if (isConstTableLookup(rhs, lookupTables))
|
|
2014
|
+
continue;
|
|
1926
2015
|
for (const { pattern, type } of patterns) {
|
|
1927
2016
|
if (pattern.test(rhs)) {
|
|
1928
2017
|
const alreadyExists = sources.some(s => s.line === lineNumber && s.type === type);
|
|
@@ -1952,6 +2041,174 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
1952
2041
|
// event-name allowlist to avoid firing on unrelated `.on('drain', cb)`
|
|
1953
2042
|
// / `.on('close', cb)` etc. that carry non-user-input.
|
|
1954
2043
|
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
2044
|
+
if (language === 'javascript') {
|
|
2045
|
+
sources.push(...findExpressDestructuredRequestSources(sourceCode));
|
|
2046
|
+
sources.push(...findGraphqlResolverArgSources(sourceCode));
|
|
2047
|
+
}
|
|
2048
|
+
return sources;
|
|
2049
|
+
}
|
|
2050
|
+
/** Index just past the `}` matching the `{` at `open`, or -1. */
|
|
2051
|
+
function matchBrace(text, open) {
|
|
2052
|
+
let depth = 0;
|
|
2053
|
+
for (let i = open; i < text.length; i++) {
|
|
2054
|
+
const ch = text[i];
|
|
2055
|
+
if (ch === '{')
|
|
2056
|
+
depth++;
|
|
2057
|
+
else if (ch === '}') {
|
|
2058
|
+
depth--;
|
|
2059
|
+
if (depth === 0)
|
|
2060
|
+
return i + 1;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return -1;
|
|
2064
|
+
}
|
|
2065
|
+
const EXPRESS_REQUEST_KEYS = {
|
|
2066
|
+
query: 'http_param',
|
|
2067
|
+
params: 'http_param',
|
|
2068
|
+
body: 'http_body',
|
|
2069
|
+
headers: 'http_header',
|
|
2070
|
+
cookies: 'http_cookie',
|
|
2071
|
+
};
|
|
2072
|
+
/**
|
|
2073
|
+
* cognium-dev#587 — Express handler whose first parameter destructures the
|
|
2074
|
+
* request: `app.get(path, ({ query }, res) => …)`, `({ query: q }, res)`,
|
|
2075
|
+
* `({ query: { id } }, res)`. Seeds the bound names of the allowlisted request
|
|
2076
|
+
* keys only. Gated on a receiver bound to `express()` / `express.Router()` /
|
|
2077
|
+
* `Router()` in the same file, so an arbitrary `({ query }) => …` callback is
|
|
2078
|
+
* not a source.
|
|
2079
|
+
*/
|
|
2080
|
+
function findExpressDestructuredRequestSources(sourceCode) {
|
|
2081
|
+
const sources = [];
|
|
2082
|
+
const receivers = new Set();
|
|
2083
|
+
for (const m of sourceCode.matchAll(/\b(?:const|let|var)\s+(\w+)\s*=\s*(?:express\s*\(\s*\)|express\s*\.\s*Router\s*\(\s*\)|Router\s*\(\s*\))/g)) {
|
|
2084
|
+
receivers.add(m[1]);
|
|
2085
|
+
}
|
|
2086
|
+
if (receivers.size === 0)
|
|
2087
|
+
return sources;
|
|
2088
|
+
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;
|
|
2089
|
+
for (const m of sourceCode.matchAll(REG_RE)) {
|
|
2090
|
+
if (!receivers.has(m[1]))
|
|
2091
|
+
continue;
|
|
2092
|
+
const open = m.index + m[0].length - 1;
|
|
2093
|
+
const close = matchBrace(sourceCode, open);
|
|
2094
|
+
if (close < 0)
|
|
2095
|
+
continue;
|
|
2096
|
+
// The pattern must be followed by another parameter or `)`, i.e. it is
|
|
2097
|
+
// the handler's first parameter.
|
|
2098
|
+
if (!/^\s*[,)]/.test(sourceCode.slice(close)))
|
|
2099
|
+
continue;
|
|
2100
|
+
const line = sourceCode.slice(0, m.index + m[0].length).split('\n').length;
|
|
2101
|
+
const inner = sourceCode.slice(open + 1, close - 1);
|
|
2102
|
+
// Split top-level entries.
|
|
2103
|
+
const entries = [];
|
|
2104
|
+
let depth = 0;
|
|
2105
|
+
let start = 0;
|
|
2106
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2107
|
+
if (inner[i] === '{')
|
|
2108
|
+
depth++;
|
|
2109
|
+
else if (inner[i] === '}')
|
|
2110
|
+
depth--;
|
|
2111
|
+
else if (inner[i] === ',' && depth === 0) {
|
|
2112
|
+
entries.push(inner.slice(start, i));
|
|
2113
|
+
start = i + 1;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
entries.push(inner.slice(start));
|
|
2117
|
+
for (const raw of entries) {
|
|
2118
|
+
const entry = raw.trim();
|
|
2119
|
+
const km = entry.match(/^(\w+)\s*(?::\s*([\s\S]+))?$/);
|
|
2120
|
+
if (!km)
|
|
2121
|
+
continue;
|
|
2122
|
+
const type = EXPRESS_REQUEST_KEYS[km[1]];
|
|
2123
|
+
if (!type)
|
|
2124
|
+
continue;
|
|
2125
|
+
const target = (km[2] ?? km[1]).replace(/=[\s\S]*$/, '').trim();
|
|
2126
|
+
const names = target.startsWith('{')
|
|
2127
|
+
? [...target.slice(1, -1).matchAll(/(?:^|,)\s*(?:\w+\s*:\s*)?(\w+)/g)].map(x => x[1])
|
|
2128
|
+
: /^\w+$/.test(target) ? [target] : [];
|
|
2129
|
+
for (const name of names) {
|
|
2130
|
+
sources.push({
|
|
2131
|
+
type,
|
|
2132
|
+
location: `Express handler destructured request '${km[1]}' -> '${name}' at line ${line}`,
|
|
2133
|
+
severity: 'high',
|
|
2134
|
+
line,
|
|
2135
|
+
confidence: 0.95,
|
|
2136
|
+
variable: name,
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return sources;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* cognium-dev#586 — GraphQL resolver-map arguments. The second parameter of a
|
|
2145
|
+
* function under `Query` / `Mutation` / `Subscription` in the object passed as
|
|
2146
|
+
* `resolvers` to `new ApolloServer({…})` / `makeExecutableSchema({…})` (inline,
|
|
2147
|
+
* or a same-file `const resolvers = {…}`) is client-supplied. Parameter 0
|
|
2148
|
+
* (`parent`) and 2 (`context`) are never seeded.
|
|
2149
|
+
*/
|
|
2150
|
+
function findGraphqlResolverArgSources(sourceCode) {
|
|
2151
|
+
const sources = [];
|
|
2152
|
+
const ctor = /(?:new\s+ApolloServer|makeExecutableSchema)\s*\(\s*\{/g;
|
|
2153
|
+
const maps = [];
|
|
2154
|
+
for (const m of sourceCode.matchAll(ctor)) {
|
|
2155
|
+
const open = m.index + m[0].length - 1;
|
|
2156
|
+
const close = matchBrace(sourceCode, open);
|
|
2157
|
+
if (close < 0)
|
|
2158
|
+
continue;
|
|
2159
|
+
const opts = sourceCode.slice(open, close);
|
|
2160
|
+
const inline = opts.match(/\bresolvers\s*:\s*\{/);
|
|
2161
|
+
if (inline) {
|
|
2162
|
+
maps.push(open + inline.index + inline[0].length - 1);
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
const named = opts.match(/\bresolvers\s*(?::\s*(\w+))?\s*[,}]/);
|
|
2166
|
+
if (!named)
|
|
2167
|
+
continue;
|
|
2168
|
+
const ident = named[1] ?? 'resolvers';
|
|
2169
|
+
const decl = new RegExp(`\\b(?:const|let|var)\\s+${ident}\\s*=\\s*\\{`).exec(sourceCode);
|
|
2170
|
+
if (decl)
|
|
2171
|
+
maps.push(decl.index + decl[0].length - 1);
|
|
2172
|
+
}
|
|
2173
|
+
const FN_RE = /(?:^|[\s,{])(\w+)\s*(?::\s*(?:async\s+)?(?:function\s*\w*\s*)?\(|\()\s*\w+\s*,\s*(\w+)\s*[,)]/g;
|
|
2174
|
+
for (const open of maps) {
|
|
2175
|
+
const close = matchBrace(sourceCode, open);
|
|
2176
|
+
if (close < 0)
|
|
2177
|
+
continue;
|
|
2178
|
+
const body = sourceCode.slice(open, close);
|
|
2179
|
+
for (const t of body.matchAll(/\b(Query|Mutation|Subscription)\s*:\s*\{/g)) {
|
|
2180
|
+
const tOpen = open + t.index + t[0].length - 1;
|
|
2181
|
+
const tClose = matchBrace(sourceCode, tOpen);
|
|
2182
|
+
if (tClose < 0)
|
|
2183
|
+
continue;
|
|
2184
|
+
// Only direct members of the type object (depth 1).
|
|
2185
|
+
const typeBody = sourceCode.slice(tOpen + 1, tClose - 1);
|
|
2186
|
+
let depth = 0;
|
|
2187
|
+
const depthAt = [];
|
|
2188
|
+
for (let i = 0; i < typeBody.length; i++) {
|
|
2189
|
+
depthAt.push(depth);
|
|
2190
|
+
const ch = typeBody[i];
|
|
2191
|
+
if (ch === '{' || ch === '(')
|
|
2192
|
+
depth++;
|
|
2193
|
+
else if (ch === '}' || ch === ')')
|
|
2194
|
+
depth--;
|
|
2195
|
+
}
|
|
2196
|
+
for (const f of typeBody.matchAll(FN_RE)) {
|
|
2197
|
+
const at = f.index + f[0].indexOf(f[1]);
|
|
2198
|
+
if (depthAt[at] !== 0)
|
|
2199
|
+
continue;
|
|
2200
|
+
const line = sourceCode.slice(0, tOpen + 1 + at).split('\n').length;
|
|
2201
|
+
sources.push({
|
|
2202
|
+
type: 'http_param',
|
|
2203
|
+
location: `GraphQL ${t[1]}.${f[1]} resolver args '${f[2]}' at line ${line}`,
|
|
2204
|
+
severity: 'high',
|
|
2205
|
+
line,
|
|
2206
|
+
confidence: 0.9,
|
|
2207
|
+
variable: f[2],
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
1955
2212
|
return sources;
|
|
1956
2213
|
}
|
|
1957
2214
|
/**
|
|
@@ -2759,6 +3016,7 @@ export function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
2759
3016
|
return new Map();
|
|
2760
3017
|
const tainted = new Map();
|
|
2761
3018
|
const lines = sourceCode.split('\n');
|
|
3019
|
+
const lookupTables = constLookupTables(sourceCode, language);
|
|
2762
3020
|
// Seed with WebSocket `.on('message', cb)` callback-param names (#213
|
|
2763
3021
|
// second slice). Same event allowlist as findJavaScriptCallbackParamSources.
|
|
2764
3022
|
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*=>)/;
|
|
@@ -2782,6 +3040,8 @@ export function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
2782
3040
|
const [, lhs, rhs] = assignMatch;
|
|
2783
3041
|
if (['if', 'while', 'for', 'return', 'true', 'false', 'null', 'undefined', 'case'].includes(lhs))
|
|
2784
3042
|
continue;
|
|
3043
|
+
if (isConstTableLookup(rhs, lookupTables))
|
|
3044
|
+
continue; // #667
|
|
2785
3045
|
const isDirectSource = JS_TAINTED_PATTERNS.some(p => p.pattern.test(rhs));
|
|
2786
3046
|
const isTaintedPropagation = tainted.size > 0 && [...tainted.keys()].some(v => new RegExp(`\\b${v}\\b`).test(rhs));
|
|
2787
3047
|
if (isDirectSource || isTaintedPropagation)
|
|
@@ -3773,9 +4033,11 @@ function findBashRealpathPrefixGuardSanitizers(code) {
|
|
|
3773
4033
|
function findGoMapAllowlistGuardSanitizers(code) {
|
|
3774
4034
|
const sanitizers = [];
|
|
3775
4035
|
const lines = code.split('\n');
|
|
3776
|
-
// `if !<mapName>[<key>] {
|
|
3777
|
-
//
|
|
3778
|
-
|
|
4036
|
+
// `if !<mapName>[<key>] {`, or that negated lookup as one `||` disjunct
|
|
4037
|
+
// (`if err != nil || !<mapName>[u.Host] {`, #293): the block runs when any
|
|
4038
|
+
// disjunct holds, so falling past it proves the key is in the map. The key
|
|
4039
|
+
// may be a selector (`u.Host`). Captures the map identifier in group 1.
|
|
4040
|
+
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*(?:\|\|[^{&]+)?\{/;
|
|
3779
4041
|
// Allow-list naming heuristic.
|
|
3780
4042
|
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
3781
4043
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3811,9 +4073,24 @@ function findGoMapAllowlistGuardSanitizers(code) {
|
|
|
3811
4073
|
if (closeLine === -1 || !bodyHasTerminator)
|
|
3812
4074
|
continue;
|
|
3813
4075
|
// Emit per-line sanitizers for every line AFTER the guard's closing
|
|
3814
|
-
// brace
|
|
3815
|
-
//
|
|
3816
|
-
|
|
4076
|
+
// brace to the end of the enclosing block (the function body, #293 —
|
|
4077
|
+
// it used to run to the end of the file, crediting later functions).
|
|
4078
|
+
// `checkSanitized` is line-keyed so this suppresses the relevant sink
|
|
4079
|
+
// types in the safe branch.
|
|
4080
|
+
let end = lines.length;
|
|
4081
|
+
for (let j = closeLine + 1, d = 0; j < lines.length; j++) {
|
|
4082
|
+
for (const ch of lines[j]) {
|
|
4083
|
+
if (ch === '{')
|
|
4084
|
+
d++;
|
|
4085
|
+
else if (ch === '}')
|
|
4086
|
+
d--;
|
|
4087
|
+
}
|
|
4088
|
+
if (d < 0) {
|
|
4089
|
+
end = j + 1;
|
|
4090
|
+
break;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
for (let l = closeLine + 2; l <= end; l++) {
|
|
3817
4094
|
sanitizers.push({
|
|
3818
4095
|
type: 'go_map_allowlist_guard',
|
|
3819
4096
|
method: 'if',
|
|
@@ -5625,6 +5902,11 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
5625
5902
|
continue;
|
|
5626
5903
|
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
5627
5904
|
continue;
|
|
5905
|
+
// cognium-dev#618: a string-prefix check against a canonical base with no
|
|
5906
|
+
// trailing separator is the prefix-confusion bypass, not a guard.
|
|
5907
|
+
const base = javaCallArgument(lines.slice(i).join('\n'), m.index + m[0].length - 1);
|
|
5908
|
+
if (base !== null && javaCanonicalBaseLacksSeparator(lines, i, base))
|
|
5909
|
+
continue;
|
|
5628
5910
|
const guarded = new Set([m[1]]);
|
|
5629
5911
|
const mentions = (t) => {
|
|
5630
5912
|
for (const n of guarded)
|
|
@@ -5658,6 +5940,147 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
5658
5940
|
}
|
|
5659
5941
|
return sanitizers;
|
|
5660
5942
|
}
|
|
5943
|
+
/**
|
|
5944
|
+
* Identifiers allowed in the initializer of a constant allowlist collection.
|
|
5945
|
+
* Anything else (a method parameter, a request read, a config lookup) means
|
|
5946
|
+
* the membership set is not a fixed set of literals.
|
|
5947
|
+
*/
|
|
5948
|
+
const JAVA_LITERAL_COLLECTION_IDENTS = new Set([
|
|
5949
|
+
'new', 'String', 'Set', 'List', 'of', 'copyOf', 'Arrays', 'asList',
|
|
5950
|
+
'HashSet', 'TreeSet', 'LinkedHashSet', 'ArrayList', 'LinkedList',
|
|
5951
|
+
'Collections', 'unmodifiableSet', 'unmodifiableList',
|
|
5952
|
+
'ImmutableSet', 'ImmutableList', 'java', 'util',
|
|
5953
|
+
]);
|
|
5954
|
+
/**
|
|
5955
|
+
* cognium-dev#668 — Java reflection allowlist guard.
|
|
5956
|
+
*
|
|
5957
|
+
* private static final Set<String> ALLOWED = Set.of("a.Csv", "a.Json");
|
|
5958
|
+
* if (!ALLOWED.contains(cls)) { throw new SecurityException("denied"); }
|
|
5959
|
+
* return Class.forName(cls).getDeclaredConstructor().newInstance();
|
|
5960
|
+
*
|
|
5961
|
+
* Past a rejecting `!<SET>.contains(<v>)` guard, `<v>` is one of the set's
|
|
5962
|
+
* literals, so the class it names is chosen by the code, not the requester.
|
|
5963
|
+
* Positive evidence only: `<SET>` must be a `final` field or local of this
|
|
5964
|
+
* file whose initializer is nothing but string literals inside the standard
|
|
5965
|
+
* collection factories, and the guard must reject (throw / return). Credits
|
|
5966
|
+
* `<v>` on later lines of the enclosing method until `<v>` is reassigned.
|
|
5967
|
+
* Scoped to `code_injection` (reflective class loading); other sink types
|
|
5968
|
+
* are untouched.
|
|
5969
|
+
*/
|
|
5970
|
+
function findJavaReflectionAllowlistGuardSanitizers(code, types) {
|
|
5971
|
+
const sanitizers = [];
|
|
5972
|
+
const lines = code.split('\n');
|
|
5973
|
+
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*\)/;
|
|
5974
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
5975
|
+
// Factory / constructor tokens that prove the initializer is a collection of
|
|
5976
|
+
// literals. A bare `final String ALLOWED = "…"` leaves no idents after string
|
|
5977
|
+
// stripping (so `.every` would pass) and `String.contains` is substring — not
|
|
5978
|
+
// an allowlist. Require at least one of these.
|
|
5979
|
+
const LITERAL_COLLECTION_FACTORIES = new Set([
|
|
5980
|
+
'of', 'copyOf', 'asList', 'HashSet', 'TreeSet', 'LinkedHashSet',
|
|
5981
|
+
'ArrayList', 'LinkedList', 'unmodifiableSet', 'unmodifiableList',
|
|
5982
|
+
'ImmutableSet', 'ImmutableList',
|
|
5983
|
+
]);
|
|
5984
|
+
/**
|
|
5985
|
+
* Nearest declaration of `name` at or above `fromLine` (0-based). A closer
|
|
5986
|
+
* non-final binding (local, `var`, parameter) wins over an earlier `final`
|
|
5987
|
+
* field; only a nearest `final` literal collection returns true.
|
|
5988
|
+
*/
|
|
5989
|
+
const isLiteralCollection = (name, fromLine) => {
|
|
5990
|
+
const finalDeclRe = new RegExp(`\\bfinal\\b[^=;(]*\\b${name}\\s*=(?!=)`);
|
|
5991
|
+
// Type/var declaration or use as a parameter (`Type name` before `=` / `;` / `,` / `)`).
|
|
5992
|
+
const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]?\\s]*|var)\\s+${name}\\s*[=;,)]`);
|
|
5993
|
+
for (let i = fromLine; i >= 0; i--) {
|
|
5994
|
+
const line = lines[i];
|
|
5995
|
+
if (!anyDeclRe.test(line) && !finalDeclRe.test(line))
|
|
5996
|
+
continue;
|
|
5997
|
+
const dm = finalDeclRe.exec(line);
|
|
5998
|
+
if (!dm)
|
|
5999
|
+
return false; // nearer non-final binding — not a fixed allowlist
|
|
6000
|
+
// Initializer up to the terminating `;` (may span a few lines).
|
|
6001
|
+
let init = line.slice(dm.index + dm[0].length);
|
|
6002
|
+
for (let j = i + 1; !init.includes(';') && j < Math.min(lines.length, i + 12); j++)
|
|
6003
|
+
init += ' ' + lines[j];
|
|
6004
|
+
if (!init.includes(';'))
|
|
6005
|
+
return false;
|
|
6006
|
+
init = init.slice(0, init.indexOf(';')).replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
6007
|
+
if (!init.includes('""'))
|
|
6008
|
+
return false;
|
|
6009
|
+
const idents = init.match(/[A-Za-z_]\w*/g) ?? [];
|
|
6010
|
+
// Empty idents (string-only initializer) or no collection factory → not a set.
|
|
6011
|
+
if (!idents.some(id => LITERAL_COLLECTION_FACTORIES.has(id)))
|
|
6012
|
+
return false;
|
|
6013
|
+
if (!idents.every(id => JAVA_LITERAL_COLLECTION_IDENTS.has(id)))
|
|
6014
|
+
return false;
|
|
6015
|
+
// A mutable collection that is written anywhere is not a fixed set.
|
|
6016
|
+
const mutRe = new RegExp(`\\b${name}\\s*\\.\\s*(?:add|addAll|put|putAll|set|replaceAll)\\s*\\(`);
|
|
6017
|
+
return !lines.some(l => mutRe.test(l));
|
|
6018
|
+
}
|
|
6019
|
+
return false;
|
|
6020
|
+
};
|
|
6021
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6022
|
+
const m = guardRe.exec(lines[i]);
|
|
6023
|
+
if (!m)
|
|
6024
|
+
continue;
|
|
6025
|
+
const [, setName, guarded] = m;
|
|
6026
|
+
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
6027
|
+
continue;
|
|
6028
|
+
if (!isLiteralCollection(setName, i))
|
|
6029
|
+
continue;
|
|
6030
|
+
const method = types
|
|
6031
|
+
.flatMap(t => t.methods)
|
|
6032
|
+
.filter(mt => mt.start_line <= i + 1 && mt.end_line >= i + 1)
|
|
6033
|
+
.sort((a, b) => (a.end_line - a.start_line) - (b.end_line - b.start_line))[0];
|
|
6034
|
+
if (!method)
|
|
6035
|
+
continue;
|
|
6036
|
+
// The guard must sit in the method body's top-level block, so it dominates
|
|
6037
|
+
// every later line (not inside an `if` branch, loop or lambda body).
|
|
6038
|
+
const before = [...lines.slice(method.start_line - 1, i), lines[i].slice(0, m.index)]
|
|
6039
|
+
.join('\n').replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
6040
|
+
const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length;
|
|
6041
|
+
if (depth !== 1)
|
|
6042
|
+
continue;
|
|
6043
|
+
// Braceless `if`/`for`/`while`/`else` keeps brace-depth 1, so also reject a
|
|
6044
|
+
// guard that is the body of a preceding braceless control statement — on the
|
|
6045
|
+
// same line (`if (strict) if (!SET.contains(v))`) or the previous statement
|
|
6046
|
+
// (`} else` / `else` / `if (x)` without `{`).
|
|
6047
|
+
const samePrefix = lines[i].slice(0, m.index).replace(/\/\/.*$/, '').trim();
|
|
6048
|
+
let prev = '';
|
|
6049
|
+
for (let p = i - 1; p >= method.start_line - 1; p--) {
|
|
6050
|
+
const t = lines[p].replace(/\/\/.*$/, '').trim();
|
|
6051
|
+
if (!t)
|
|
6052
|
+
continue;
|
|
6053
|
+
prev = t;
|
|
6054
|
+
break;
|
|
6055
|
+
}
|
|
6056
|
+
const openBraceless = (s) => {
|
|
6057
|
+
// Allow a leading `}` so `} else` counts as braceless else.
|
|
6058
|
+
const t = s.replace(/^\}+\s*/, '').trim();
|
|
6059
|
+
return /^(?:else(?:\s+if\b)?|if|for|while)\b/.test(t) && !s.includes('{');
|
|
6060
|
+
};
|
|
6061
|
+
if (openBraceless(samePrefix) || (prev && openBraceless(prev)))
|
|
6062
|
+
continue;
|
|
6063
|
+
const refRe = new RegExp(`(?<![\\w.])${guarded}\\b`);
|
|
6064
|
+
// Credit only lines where `<v>` is a whole call argument, `f(<v>)`, so a
|
|
6065
|
+
// second tainted value on the same line (`forName(v + other)`) still reports.
|
|
6066
|
+
const wholeArgRe = new RegExp(`\\(\\s*${guarded}\\s*\\)`);
|
|
6067
|
+
const reassignRe = new RegExp(`(?<![\\w.])${guarded}\\s*(?:=(?!=)|\\+=)`);
|
|
6068
|
+
for (let l = i + 1; l < Math.min(method.end_line, lines.length); l++) {
|
|
6069
|
+
const lt = lines[l];
|
|
6070
|
+
if (reassignRe.test(lt))
|
|
6071
|
+
break;
|
|
6072
|
+
if (!refRe.test(lt) || !wholeArgRe.test(lt))
|
|
6073
|
+
continue;
|
|
6074
|
+
sanitizers.push({
|
|
6075
|
+
type: 'java_reflection_allowlist_guard',
|
|
6076
|
+
method: 'contains',
|
|
6077
|
+
line: l + 1,
|
|
6078
|
+
sanitizes: ['code_injection'],
|
|
6079
|
+
});
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
return sanitizers;
|
|
6083
|
+
}
|
|
5661
6084
|
/**
|
|
5662
6085
|
* Does the `if (...)` guard on `lines[i]` actually **reject**?
|
|
5663
6086
|
*
|
|
@@ -5733,6 +6156,212 @@ function guardRejects(lines, i, terminatorRe, window) {
|
|
|
5733
6156
|
* form `if (x.StartsWith(root)) { … }` still reports, and a test pins that so
|
|
5734
6157
|
* the gap stays visible rather than silently assumed.
|
|
5735
6158
|
*/
|
|
6159
|
+
/**
|
|
6160
|
+
* cognium-dev#293 (Java cell): membership in a literal allow-list followed by
|
|
6161
|
+
* a reject is a guard.
|
|
6162
|
+
*
|
|
6163
|
+
* if (!Set.of("locale", "theme").contains(key)) throw new IllegalArgumentException();
|
|
6164
|
+
* req.getSession().setAttribute(key, value); // was trust_boundary on `key`
|
|
6165
|
+
*
|
|
6166
|
+
* Past the guard, `key` is one of the listed string constants. The set must
|
|
6167
|
+
* be written out as string literals: inline `Set.of` / `List.of` /
|
|
6168
|
+
* `Arrays.asList`, or a `static final` constant of the same file initialised
|
|
6169
|
+
* from one of them. A set built any other way (a parameter, a config read, a
|
|
6170
|
+
* mutable collection) is not credited: its contents are unknown.
|
|
6171
|
+
*
|
|
6172
|
+
* Only the guarded variable is credited, from the guard to the end of the
|
|
6173
|
+
* enclosing method or the variable's next reassignment, and only when the
|
|
6174
|
+
* guard rejects (`guardRejects`).
|
|
6175
|
+
*/
|
|
6176
|
+
const JAVA_OPERAND_KEYWORDS = new Set([
|
|
6177
|
+
'new', 'return', 'this', 'super', 'null', 'true', 'false', 'throw', 'if', 'else', 'instanceof',
|
|
6178
|
+
'int', 'long', 'short', 'byte', 'char', 'boolean', 'double', 'float', 'var', 'final',
|
|
6179
|
+
]);
|
|
6180
|
+
/**
|
|
6181
|
+
* True when `expr` is only `name`, string literals, `.length()` / `.size()`,
|
|
6182
|
+
* and `+` (no other locals or value-producing calls).
|
|
6183
|
+
*/
|
|
6184
|
+
function javaExprOnlyGuardedAndLits(expr, name) {
|
|
6185
|
+
let s = expr
|
|
6186
|
+
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '')
|
|
6187
|
+
.replace(/\.\s*(?:length|size)\s*\(\s*\)/g, '')
|
|
6188
|
+
.replace(new RegExp(String.raw `\b${name}\b`, 'g'), '');
|
|
6189
|
+
s = s.replace(/[+\s()]/g, '');
|
|
6190
|
+
return s.length === 0;
|
|
6191
|
+
}
|
|
6192
|
+
/** Top-level comma-separated arguments of a call, respecting quotes and nesting. */
|
|
6193
|
+
function javaSplitCallArgs(argsText) {
|
|
6194
|
+
const out = [];
|
|
6195
|
+
let depth = 0;
|
|
6196
|
+
let quote = null;
|
|
6197
|
+
let start = 0;
|
|
6198
|
+
for (let i = 0; i < argsText.length; i++) {
|
|
6199
|
+
const c = argsText[i];
|
|
6200
|
+
if (quote) {
|
|
6201
|
+
if (c === '\\') {
|
|
6202
|
+
i++;
|
|
6203
|
+
continue;
|
|
6204
|
+
}
|
|
6205
|
+
if (c === quote)
|
|
6206
|
+
quote = null;
|
|
6207
|
+
continue;
|
|
6208
|
+
}
|
|
6209
|
+
if (c === '"' || c === "'") {
|
|
6210
|
+
quote = c;
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
if (c === '(' || c === '[' || c === '{')
|
|
6214
|
+
depth++;
|
|
6215
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
6216
|
+
depth--;
|
|
6217
|
+
else if (c === ',' && depth === 0) {
|
|
6218
|
+
out.push(argsText.slice(start, i).trim());
|
|
6219
|
+
start = i + 1;
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
const last = argsText.slice(start).trim();
|
|
6223
|
+
if (last || out.length > 0)
|
|
6224
|
+
out.push(last);
|
|
6225
|
+
return out;
|
|
6226
|
+
}
|
|
6227
|
+
/**
|
|
6228
|
+
* True when a call on `line` (other than length/size) has an argument that is
|
|
6229
|
+
* not only string literals and the guarded variable — a raw operand.
|
|
6230
|
+
*/
|
|
6231
|
+
function javaCallHasRawArgument(line, name) {
|
|
6232
|
+
const re = /\b([A-Za-z_]\w*)\s*\(/g;
|
|
6233
|
+
let m;
|
|
6234
|
+
while ((m = re.exec(line)) !== null) {
|
|
6235
|
+
const method = m[1];
|
|
6236
|
+
if (method === 'length' || method === 'size')
|
|
6237
|
+
continue;
|
|
6238
|
+
const open = m.index + m[0].length - 1;
|
|
6239
|
+
let depth = 0;
|
|
6240
|
+
let quote = null;
|
|
6241
|
+
let end = -1;
|
|
6242
|
+
for (let i = open; i < line.length; i++) {
|
|
6243
|
+
const c = line[i];
|
|
6244
|
+
if (quote) {
|
|
6245
|
+
if (c === '\\') {
|
|
6246
|
+
i++;
|
|
6247
|
+
continue;
|
|
6248
|
+
}
|
|
6249
|
+
if (c === quote)
|
|
6250
|
+
quote = null;
|
|
6251
|
+
continue;
|
|
6252
|
+
}
|
|
6253
|
+
if (c === '"' || c === "'") {
|
|
6254
|
+
quote = c;
|
|
6255
|
+
continue;
|
|
6256
|
+
}
|
|
6257
|
+
if (c === '(')
|
|
6258
|
+
depth++;
|
|
6259
|
+
else if (c === ')') {
|
|
6260
|
+
depth--;
|
|
6261
|
+
if (depth === 0) {
|
|
6262
|
+
end = i;
|
|
6263
|
+
break;
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
if (end < 0)
|
|
6268
|
+
continue;
|
|
6269
|
+
for (const arg of javaSplitCallArgs(line.slice(open + 1, end))) {
|
|
6270
|
+
if (!arg)
|
|
6271
|
+
continue;
|
|
6272
|
+
if (!javaExprOnlyGuardedAndLits(arg, name))
|
|
6273
|
+
return true;
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
return false;
|
|
6277
|
+
}
|
|
6278
|
+
/**
|
|
6279
|
+
* True when `line` reads a lower-case identifier other than `name` as a value
|
|
6280
|
+
* (not a receiver, call name, member, type, constant or the assigned local),
|
|
6281
|
+
* or a call whose argument is not only string literals / `name` (a call-shaped
|
|
6282
|
+
* raw operand such as `req.getParameter("level")` beside the guarded local).
|
|
6283
|
+
*/
|
|
6284
|
+
function otherLocalOperand(line, name) {
|
|
6285
|
+
if (javaCallHasRawArgument(line, name))
|
|
6286
|
+
return true;
|
|
6287
|
+
const s = line
|
|
6288
|
+
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""')
|
|
6289
|
+
.replace(/^\s*(?:[\w<>[\],.?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/, ' ')
|
|
6290
|
+
.replace(/\.\s*[A-Za-z_]\w*/g, ' ')
|
|
6291
|
+
.replace(/\b[A-Za-z_]\w*(?=\s*[.(])/g, ' ');
|
|
6292
|
+
return (s.match(/\b[a-z_]\w*\b/g) ?? []).some(id => id !== name && !JAVA_OPERAND_KEYWORDS.has(id));
|
|
6293
|
+
}
|
|
6294
|
+
function findJavaLiteralAllowlistGuardSanitizers(code) {
|
|
6295
|
+
const sanitizers = [];
|
|
6296
|
+
const lines = code.split('\n');
|
|
6297
|
+
const literalList = String.raw `(?:Set|List)\s*\.\s*of|Arrays\s*\.\s*asList`;
|
|
6298
|
+
const literalArgs = String.raw `\(\s*"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*\s*\)`;
|
|
6299
|
+
// Constants declared from a literal list in this file.
|
|
6300
|
+
const constants = new Set();
|
|
6301
|
+
const constRe = new RegExp(String.raw `\bstatic\s+final\s+[\w<>,.\s]+?\s([A-Za-z_]\w*)\s*=\s*(?:${literalList})\s*${literalArgs}\s*;`);
|
|
6302
|
+
for (const l of lines) {
|
|
6303
|
+
const m = constRe.exec(l);
|
|
6304
|
+
if (m)
|
|
6305
|
+
constants.add(m[1]);
|
|
6306
|
+
}
|
|
6307
|
+
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*\)`);
|
|
6308
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
6309
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6310
|
+
const m = guardRe.exec(lines[i]);
|
|
6311
|
+
if (!m)
|
|
6312
|
+
continue;
|
|
6313
|
+
if (m[1] !== undefined && !constants.has(m[1]))
|
|
6314
|
+
continue;
|
|
6315
|
+
if (!guardRejects(lines, i, terminatorRe, 6))
|
|
6316
|
+
continue;
|
|
6317
|
+
const name = m[2];
|
|
6318
|
+
const mention = new RegExp(String.raw `(?<![\w.])${name}\b`);
|
|
6319
|
+
const reassign = new RegExp(String.raw `^\s*(?:[\w.<>[\]]+\s+)?${name}\s*=(?!=)`);
|
|
6320
|
+
// Skip the guard's own block, then credit to the end of the enclosing method.
|
|
6321
|
+
let start = i + 1;
|
|
6322
|
+
if (/\{\s*$/.test(lines[i])) {
|
|
6323
|
+
for (let d = 0, j = i; j < lines.length; j++) {
|
|
6324
|
+
for (const ch of lines[j]) {
|
|
6325
|
+
if (ch === '{')
|
|
6326
|
+
d++;
|
|
6327
|
+
else if (ch === '}')
|
|
6328
|
+
d--;
|
|
6329
|
+
}
|
|
6330
|
+
if (d <= 0) {
|
|
6331
|
+
start = j + 1;
|
|
6332
|
+
break;
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
for (let j = start, d = 0; j < lines.length; j++) {
|
|
6337
|
+
for (const ch of lines[j]) {
|
|
6338
|
+
if (ch === '{')
|
|
6339
|
+
d++;
|
|
6340
|
+
else if (ch === '}')
|
|
6341
|
+
d--;
|
|
6342
|
+
}
|
|
6343
|
+
if (d < 0)
|
|
6344
|
+
break;
|
|
6345
|
+
if (reassign.test(lines[j]))
|
|
6346
|
+
break;
|
|
6347
|
+
if (!mention.test(lines[j]))
|
|
6348
|
+
continue;
|
|
6349
|
+
// The credit is line-keyed: a line that also carries another local
|
|
6350
|
+
// (`"... n='" + who + "' ORDER BY " + col`) would clean `who` too.
|
|
6351
|
+
if (otherLocalOperand(lines[j], name))
|
|
6352
|
+
continue;
|
|
6353
|
+
sanitizers.push({
|
|
6354
|
+
type: 'java_literal_allowlist_guard',
|
|
6355
|
+
method: 'guard',
|
|
6356
|
+
line: j + 1,
|
|
6357
|
+
sanitizes: ['trust_boundary', 'sql_injection', 'command_injection', 'path_traversal', 'ssrf',
|
|
6358
|
+
'ldap_injection', 'xpath_injection', 'log_injection', 'xss', 'open_redirect',
|
|
6359
|
+
'external_taint_escape'],
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
return sanitizers;
|
|
6364
|
+
}
|
|
5736
6365
|
/**
|
|
5737
6366
|
* cognium-dev#272 — a validating predicate followed by an EARLY RETURN is a
|
|
5738
6367
|
* guard, and the value reaching the sink afterwards is already validated.
|
|
@@ -6000,6 +6629,19 @@ function findJavaPathGetFileNameSanitizers(code) {
|
|
|
6000
6629
|
* strips CRLF. In Java that means an explicit `String`/`CharSequence` return
|
|
6001
6630
|
* type, which keeps `void` log-and-forget methods out.
|
|
6002
6631
|
*/
|
|
6632
|
+
/** True when the Java text strips both `\r` and `\n` via replace / replaceAll (cognium-dev#605). */
|
|
6633
|
+
function javaBodyStripsCrAndLf(body) {
|
|
6634
|
+
let cr = false;
|
|
6635
|
+
let lf = false;
|
|
6636
|
+
for (const m of body.matchAll(/\.\s*(replace|replaceAll)\s*\(\s*(?:"((?:\\.|[^"\\])*)"|'(\\.|[^'\\])')/g)) {
|
|
6637
|
+
const cov = javaReplaceCrLfCoverage(m[1], m[2] ?? m[3] ?? '');
|
|
6638
|
+
if (cov.cr)
|
|
6639
|
+
cr = true;
|
|
6640
|
+
if (cov.lf)
|
|
6641
|
+
lf = true;
|
|
6642
|
+
}
|
|
6643
|
+
return cr && lf;
|
|
6644
|
+
}
|
|
6003
6645
|
function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
6004
6646
|
const sanitizers = [];
|
|
6005
6647
|
const lines = code.split('\n');
|
|
@@ -6016,8 +6658,25 @@ function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
|
6016
6658
|
const m = declRe.exec(lines[i]);
|
|
6017
6659
|
if (!m)
|
|
6018
6660
|
continue;
|
|
6019
|
-
|
|
6020
|
-
|
|
6661
|
+
if (language !== 'java') {
|
|
6662
|
+
const body = lines.slice(i, Math.min(lines.length, i + bodyWindow)).join('\n');
|
|
6663
|
+
if (stripRe.test(body))
|
|
6664
|
+
wrappers.add(m[1]);
|
|
6665
|
+
continue;
|
|
6666
|
+
}
|
|
6667
|
+
// Java: stop at the next declaration so one helper's body does not
|
|
6668
|
+
// credit the helper above it, and require the strip to cover BOTH CR and
|
|
6669
|
+
// LF. An LF-only `replace("\n", "")` leaves CR, which still forges a
|
|
6670
|
+
// log line (cognium-dev#605).
|
|
6671
|
+
let end = Math.min(lines.length, i + bodyWindow);
|
|
6672
|
+
for (let j = i + 1; j < end; j++) {
|
|
6673
|
+
if (declRe.test(lines[j])) {
|
|
6674
|
+
end = j;
|
|
6675
|
+
break;
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
const body = lines.slice(i, end).join('\n');
|
|
6679
|
+
if (javaBodyStripsCrAndLf(body))
|
|
6021
6680
|
wrappers.add(m[1]);
|
|
6022
6681
|
}
|
|
6023
6682
|
if (wrappers.size === 0)
|
|
@@ -7108,8 +7767,13 @@ export function findJsPatternFindings(code, file) {
|
|
|
7108
7767
|
* the args is wrapped in a recognized HTML escaper
|
|
7109
7768
|
* (`html.EscapeString` / `template.HTMLEscapeString` /
|
|
7110
7769
|
* `template.HTMLEscaper`).
|
|
7770
|
+
*
|
|
7771
|
+
* A write whose every argument is a local holding an int or bool measure of
|
|
7772
|
+
* a value (`scalarVarsAt(line)`, #568 — `fmt.Fprintf(w, "%d", n)` with
|
|
7773
|
+
* `n := len(...)`) is skipped. Any other argument, including request reads
|
|
7774
|
+
* the taint engine has no source for (`r.URL.Path`), still reports.
|
|
7111
7775
|
*/
|
|
7112
|
-
export function findGoXssFindings(code, file) {
|
|
7776
|
+
export function findGoXssFindings(code, file, scalarVarsAt = () => new Set()) {
|
|
7113
7777
|
const out = [];
|
|
7114
7778
|
const lines = code.split('\n');
|
|
7115
7779
|
const rwNames = new Set();
|
|
@@ -7140,6 +7804,11 @@ export function findGoXssFindings(code, file) {
|
|
|
7140
7804
|
// Require at least one identifier in tail outside string literals.
|
|
7141
7805
|
if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, '')))
|
|
7142
7806
|
continue;
|
|
7807
|
+
const idents = tail.replace(/"(?:[^"\\]|\\.)*"|`[^`]*`/g, '')
|
|
7808
|
+
.match(/[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/g) ?? [];
|
|
7809
|
+
const scalars = scalarVarsAt(i + 1);
|
|
7810
|
+
if (idents.length > 0 && idents.every(id => scalars.has(id)))
|
|
7811
|
+
continue;
|
|
7143
7812
|
out.push({
|
|
7144
7813
|
id: `xss-${file}-${i + 1}`,
|
|
7145
7814
|
pass: 'language-sources',
|
|
@@ -10504,4 +11173,219 @@ export function findJsTemplateInjectionSstiFindings(code, file) {
|
|
|
10504
11173
|
}
|
|
10505
11174
|
return findings;
|
|
10506
11175
|
}
|
|
11176
|
+
// ---------------------------------------------------------------------------
|
|
11177
|
+
// cognium-dev#566 — Spring MVC return-as-body XSS (Java)
|
|
11178
|
+
// ---------------------------------------------------------------------------
|
|
11179
|
+
const SPRING_MAPPING_ANN = /^(?:Get|Post|Put|Delete|Patch|Request)Mapping\b/;
|
|
11180
|
+
const SPRING_WEB_PARAM_ANN = /^(?:RequestParam|PathVariable|RequestHeader|CookieValue|MatrixVariable)\b/;
|
|
11181
|
+
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)$/;
|
|
11182
|
+
/**
|
|
11183
|
+
* Value of `produces = …` on a `@*Mapping` annotation, or null when absent.
|
|
11184
|
+
* Only the produces value is considered — a path like `/widgets.html` must not
|
|
11185
|
+
* look like an HTML media type.
|
|
11186
|
+
*/
|
|
11187
|
+
function springProducesValue(annotations) {
|
|
11188
|
+
for (const a of annotations) {
|
|
11189
|
+
if (!/^\w+Mapping\b/.test(a) || !/\bproduces\s*=/.test(a))
|
|
11190
|
+
continue;
|
|
11191
|
+
const m = /\bproduces\s*=\s*(?:\{([^}]*)\}|([^,)]+))/.exec(a);
|
|
11192
|
+
if (!m)
|
|
11193
|
+
continue;
|
|
11194
|
+
return (m[1] ?? m[2] ?? '').trim();
|
|
11195
|
+
}
|
|
11196
|
+
return null;
|
|
11197
|
+
}
|
|
11198
|
+
/** True when a produces value names a non-HTML media type. */
|
|
11199
|
+
function springProducesValueIsNonHtml(value) {
|
|
11200
|
+
return !/html/i.test(value);
|
|
11201
|
+
}
|
|
11202
|
+
/** Markup or string concatenation around a value (the Python return-XSS gate, #368). */
|
|
11203
|
+
function javaExprLooksLikeHtml(expr) {
|
|
11204
|
+
return /"[^"]*<[^"]*"/.test(expr) || /"\s*\+/.test(expr) || /\+\s*"/.test(expr);
|
|
11205
|
+
}
|
|
11206
|
+
/** Parentheses outside string literals never close below depth 0 and end at 0. */
|
|
11207
|
+
function javaParensBalanced(expr) {
|
|
11208
|
+
let depth = 0;
|
|
11209
|
+
for (const ch of expr.replace(/"(?:[^"\\]|\\.)*"/g, '""')) {
|
|
11210
|
+
if (ch === '(')
|
|
11211
|
+
depth++;
|
|
11212
|
+
else if (ch === ')' && --depth < 0)
|
|
11213
|
+
return false;
|
|
11214
|
+
}
|
|
11215
|
+
return depth === 0;
|
|
11216
|
+
}
|
|
11217
|
+
/**
|
|
11218
|
+
* cognium-dev#566 — the value a Spring MVC handler returns is the response
|
|
11219
|
+
* body when the class is `@RestController` (or `@ResponseBody` sits on the
|
|
11220
|
+
* class or the method), and always when the handler returns a
|
|
11221
|
+
* `ResponseEntity`. A `String` body is written with the negotiated content
|
|
11222
|
+
* type, so a browser request gets it as `text/html`: request data
|
|
11223
|
+
* concatenated into markup there is reflected XSS.
|
|
11224
|
+
*
|
|
11225
|
+
* Positive evidence only:
|
|
11226
|
+
* - the method carries a `@*Mapping` annotation;
|
|
11227
|
+
* - it returns `String` under a body-writing controller, or
|
|
11228
|
+
* `ResponseEntity<String>` via `ResponseEntity.ok(x)` /
|
|
11229
|
+
* `new ResponseEntity<>(x, …)` (the builder `.body(x)` form is left to
|
|
11230
|
+
* SinkFilterPass Stage 15c's text/html content-type rule);
|
|
11231
|
+
* - the returned expression references a request-bound parameter
|
|
11232
|
+
* (`@RequestParam`, `@PathVariable`, `@RequestHeader`, `@CookieValue`,
|
|
11233
|
+
* `@MatrixVariable`, a `String` `@RequestBody`) or an in-method source,
|
|
11234
|
+
* directly or through a local assigned from it, and looks like HTML
|
|
11235
|
+
* (markup or string concatenation, directly or in that local's assignment).
|
|
11236
|
+
*
|
|
11237
|
+
* Not a sink: a plain `@Controller` returning a `String` (a view name),
|
|
11238
|
+
* `produces = ` a non-HTML media type on the method or class mapping, a
|
|
11239
|
+
* non-`String` return (serialised as JSON), a returned value that does not
|
|
11240
|
+
* look like markup, and any handler with an xss-covering sanitizer between
|
|
11241
|
+
* its start and the return.
|
|
11242
|
+
*/
|
|
11243
|
+
export function findJavaSpringReturnXSSSinks(code, types, sources, xssSanitizerLines) {
|
|
11244
|
+
const out = [];
|
|
11245
|
+
const lines = code.split('\n');
|
|
11246
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11247
|
+
const stripStrings = (s) => s.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
11248
|
+
const refers = (expr, name) => new RegExp(`(?<![\\w.])${esc(name)}\\b`).test(stripStrings(expr));
|
|
11249
|
+
for (const t of types) {
|
|
11250
|
+
const ann = t.annotations ?? [];
|
|
11251
|
+
const isRest = ann.some(a => /^RestController\b/.test(a));
|
|
11252
|
+
if (!isRest && !ann.some(a => /^Controller\b/.test(a)))
|
|
11253
|
+
continue;
|
|
11254
|
+
const classProduces = springProducesValue(ann);
|
|
11255
|
+
const classBody = isRest || ann.some(a => /^ResponseBody\b/.test(a));
|
|
11256
|
+
for (const m of t.methods) {
|
|
11257
|
+
if (!m.annotations.some(a => SPRING_MAPPING_ANN.test(a)))
|
|
11258
|
+
continue;
|
|
11259
|
+
// Method-level produces overrides class-level; only non-HTML suppresses.
|
|
11260
|
+
const methodProduces = springProducesValue(m.annotations);
|
|
11261
|
+
const produces = methodProduces ?? classProduces;
|
|
11262
|
+
if (produces != null && springProducesValueIsNonHtml(produces))
|
|
11263
|
+
continue;
|
|
11264
|
+
const rt = (m.return_type ?? '').replace(/\s+/g, '');
|
|
11265
|
+
const entity = rt === 'ResponseEntity<String>';
|
|
11266
|
+
const body = rt === 'String' && (classBody || m.annotations.some(a => /^ResponseBody\b/.test(a)));
|
|
11267
|
+
if (!entity && !body)
|
|
11268
|
+
continue;
|
|
11269
|
+
// Seeds: request-bound parameters, then in-method sources with a variable.
|
|
11270
|
+
const seeds = new Map();
|
|
11271
|
+
for (const p of m.parameters) {
|
|
11272
|
+
// Spring converts to the declared type first: a number, boolean,
|
|
11273
|
+
// UUID or date cannot carry markup.
|
|
11274
|
+
if (SPRING_NON_TEXT_PARAM_TYPE.test((p.type ?? '').replace(/^(?:[\w]+\.)+/, '')))
|
|
11275
|
+
continue;
|
|
11276
|
+
const bound = p.annotations.some(a => SPRING_WEB_PARAM_ANN.test(a))
|
|
11277
|
+
|| (p.annotations.some(a => /^RequestBody\b/.test(a)) && p.type === 'String');
|
|
11278
|
+
if (bound)
|
|
11279
|
+
seeds.set(p.name, p.line ?? m.start_line);
|
|
11280
|
+
}
|
|
11281
|
+
for (const s of sources) {
|
|
11282
|
+
// A plain parameter of the handler is not request data by itself.
|
|
11283
|
+
if (s.type === 'interprocedural_param')
|
|
11284
|
+
continue;
|
|
11285
|
+
if (!s.variable || !/^[A-Za-z_]\w*$/.test(s.variable))
|
|
11286
|
+
continue;
|
|
11287
|
+
if (s.line > m.start_line && s.line <= m.end_line && !seeds.has(s.variable))
|
|
11288
|
+
seeds.set(s.variable, s.line);
|
|
11289
|
+
}
|
|
11290
|
+
if (seeds.size === 0)
|
|
11291
|
+
continue;
|
|
11292
|
+
// Locals assigned from a seed (method body only), each tied to its seed's line.
|
|
11293
|
+
const bodyText = lines.slice(m.start_line - 1, m.end_line).join('\n');
|
|
11294
|
+
const carriers = new Map();
|
|
11295
|
+
for (const [n, l] of seeds)
|
|
11296
|
+
carriers.set(n, { sourceLine: l, assignLine: l });
|
|
11297
|
+
const derived = [...buildJavaTaintedVars(bodyText, new Set(seeds.keys()))].sort((a, b) => a[1] - b[1]);
|
|
11298
|
+
for (const [n, rel] of derived) {
|
|
11299
|
+
if (carriers.has(n))
|
|
11300
|
+
continue;
|
|
11301
|
+
const assignLine = m.start_line + rel - 1;
|
|
11302
|
+
const from = [...carriers].find(([c]) => refers(lines[assignLine - 1] ?? '', c));
|
|
11303
|
+
carriers.set(n, { sourceLine: from ? from[1].sourceLine : m.start_line, assignLine });
|
|
11304
|
+
}
|
|
11305
|
+
for (let i = m.start_line - 1; i < Math.min(m.end_line, lines.length); i++) {
|
|
11306
|
+
if (!/^\s*return\b/.test(lines[i] ?? ''))
|
|
11307
|
+
continue;
|
|
11308
|
+
let stmt = lines[i];
|
|
11309
|
+
for (let j = i + 1; !stmt.includes(';') && j < Math.min(m.end_line, lines.length); j++)
|
|
11310
|
+
stmt += ' ' + lines[j];
|
|
11311
|
+
stmt = stmt.replace(/^\s*return\b/, '').replace(/;[^;]*$/, '');
|
|
11312
|
+
const sinkLine = i + 1;
|
|
11313
|
+
let expr = stmt;
|
|
11314
|
+
if (entity) {
|
|
11315
|
+
// `ResponseEntity.ok(x)` / `new ResponseEntity<>(x, status)` only. The
|
|
11316
|
+
// builder `.body(x)` form stays with SinkFilterPass Stage 15c, which
|
|
11317
|
+
// keeps it only next to an explicit text/html signal.
|
|
11318
|
+
const em = /^\s*ResponseEntity\s*\.\s*ok\s*\(([\s\S]+)\)\s*$/.exec(stmt)
|
|
11319
|
+
?? /^\s*new\s+ResponseEntity\s*(?:<[^>]*>)?\s*\(([\s\S]+),[^,()]*\)\s*$/.exec(stmt);
|
|
11320
|
+
if (!em || !javaParensBalanced(em[1]))
|
|
11321
|
+
continue;
|
|
11322
|
+
expr = em[1];
|
|
11323
|
+
}
|
|
11324
|
+
const hit = [...carriers].find(([c, info]) => info.assignLine <= sinkLine && refers(expr, c));
|
|
11325
|
+
if (!hit)
|
|
11326
|
+
continue;
|
|
11327
|
+
const [name, info] = hit;
|
|
11328
|
+
let htmlish = javaExprLooksLikeHtml(expr);
|
|
11329
|
+
if (!htmlish && new RegExp(`^\\s*${esc(name)}\\s*$`).test(expr) && info.assignLine !== info.sourceLine) {
|
|
11330
|
+
htmlish = javaExprLooksLikeHtml(lines[info.assignLine - 1] ?? '');
|
|
11331
|
+
}
|
|
11332
|
+
if (!htmlish)
|
|
11333
|
+
continue;
|
|
11334
|
+
// Suppress only when an xss sanitizer's operand is the returned carrier
|
|
11335
|
+
// (or the carrier local was assigned from that sanitizer). Escaping a
|
|
11336
|
+
// different variable must not clear a return of another carrier.
|
|
11337
|
+
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*)/;
|
|
11338
|
+
const sanitizerCoversCarrier = xssSanitizerLines.some(l => {
|
|
11339
|
+
if (l < m.start_line || l > sinkLine)
|
|
11340
|
+
return false;
|
|
11341
|
+
const line = lines[l - 1] ?? '';
|
|
11342
|
+
const ops = [];
|
|
11343
|
+
let sm;
|
|
11344
|
+
const re = new RegExp(xssEscapeOpRe.source, 'g');
|
|
11345
|
+
while ((sm = re.exec(line)) !== null)
|
|
11346
|
+
ops.push(sm[1]);
|
|
11347
|
+
// Unparsed shapes (e.g. static-import `htmlEscape(title)`) do not suppress.
|
|
11348
|
+
if (ops.length === 0)
|
|
11349
|
+
return false;
|
|
11350
|
+
// Operand is the returned carrier.
|
|
11351
|
+
if (ops.some(op => op === name))
|
|
11352
|
+
return true;
|
|
11353
|
+
// Carrier assigned from the sanitizer call itself (`body = HtmlUtils.htmlEscape(x)`),
|
|
11354
|
+
// not a concat that merely mentions a sanitizer on the assignment line.
|
|
11355
|
+
if (l === info.assignLine) {
|
|
11356
|
+
const stripped = line.replace(/"(?:[^"\\]|\\.)*"/g, '""').trim();
|
|
11357
|
+
const am = new RegExp(`\\b${esc(name)}\\s*=\\s*(.+)$`).exec(stripped);
|
|
11358
|
+
if (!am)
|
|
11359
|
+
return false;
|
|
11360
|
+
const rhs = am[1].replace(/;\s*$/, '').trim();
|
|
11361
|
+
const call = xssEscapeOpRe.exec(rhs);
|
|
11362
|
+
if (!call || call.index !== 0)
|
|
11363
|
+
return false;
|
|
11364
|
+
// Whole RHS is one sanitizer invocation (no concatenation outside the call).
|
|
11365
|
+
let depth = 0;
|
|
11366
|
+
let ended = -1;
|
|
11367
|
+
for (let k = call.index; k < rhs.length; k++) {
|
|
11368
|
+
const ch = rhs[k];
|
|
11369
|
+
if (ch === '(')
|
|
11370
|
+
depth++;
|
|
11371
|
+
else if (ch === ')') {
|
|
11372
|
+
depth--;
|
|
11373
|
+
if (depth === 0) {
|
|
11374
|
+
ended = k;
|
|
11375
|
+
break;
|
|
11376
|
+
}
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11379
|
+
return ended >= 0 && rhs.slice(ended + 1).trim() === '';
|
|
11380
|
+
}
|
|
11381
|
+
return false;
|
|
11382
|
+
});
|
|
11383
|
+
if (sanitizerCoversCarrier)
|
|
11384
|
+
continue;
|
|
11385
|
+
out.push({ sinkLine, sourceLine: info.sourceLine });
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
return out;
|
|
11390
|
+
}
|
|
10507
11391
|
//# sourceMappingURL=language-sources-pass.js.map
|