circle-ir 4.14.2 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/const-table-lookup.d.ts +29 -0
- package/dist/analysis/const-table-lookup.d.ts.map +1 -0
- package/dist/analysis/const-table-lookup.js +145 -0
- package/dist/analysis/const-table-lookup.js.map +1 -0
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +149 -4
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/go-strings-replacer.d.ts +21 -0
- package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
- package/dist/analysis/go-strings-replacer.js +145 -0
- package/dist/analysis/go-strings-replacer.js.map +1 -0
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +148 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/java-canonical-prefix.d.ts +42 -0
- package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
- package/dist/analysis/java-canonical-prefix.js +145 -0
- package/dist/analysis/java-canonical-prefix.js.map +1 -0
- package/dist/analysis/java-crlf-strip.d.ts +30 -0
- package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/java-crlf-strip.js +195 -0
- package/dist/analysis/java-crlf-strip.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
- package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +43 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +127 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +20 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +76 -2
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +2045 -156
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +214 -6
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +509 -27
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1606 -60
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +33 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -0
- package/dist/analysis/scalar-derivation.js +368 -0
- package/dist/analysis/scalar-derivation.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +695 -34
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +375 -24
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +19 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +25539 -19200
- package/dist/core/circle-ir-core.cjs +3945 -2126
- package/dist/core/circle-ir-core.js +3945 -2126
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +336 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph/code-graph.d.ts +9 -0
- package/dist/graph/code-graph.d.ts.map +1 -1
- package/dist/graph/code-graph.js +11 -0
- package/dist/graph/code-graph.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +16 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +110 -42
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +3 -3
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Depends on: taint-matcher, constant-propagation, language-sources
|
|
17
17
|
*/
|
|
18
|
-
import { CSHARP_SANITIZER_RES, CSharpCredit } from './csharp-sanitizer-credit.js';
|
|
18
|
+
import { CSHARP_SANITIZER_RES, CSharpCredit, csharpSanitizersFor } from './csharp-sanitizer-credit.js';
|
|
19
19
|
import { JS_TAINTED_PATTERNS } from './language-sources-pass.js';
|
|
20
20
|
import { LIBRARY_API_SURFACE_TAG } from '../library-api-surface-downgrade.js';
|
|
21
21
|
import { isNonExecutableSourceLine } from '../non-executable-lines.js';
|
|
22
22
|
import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
23
23
|
import { identifierOutsideSanitizerCalls } from '../taint-propagation.js';
|
|
24
|
+
import { parseJavaArrayAccessExpression } from '../java-array-element-keys.js';
|
|
24
25
|
import { resolveIdentifierType, resolveCallReturnType, isArgvContainerType, } from '../arg-type-resolver.js';
|
|
25
26
|
/** Escape regex metacharacters for safe pattern building. */
|
|
26
27
|
function escapeReSf(s) {
|
|
@@ -60,10 +61,8 @@ const JS_LOG_INJECTION_SANITIZERS = [
|
|
|
60
61
|
/\bstripCRLF\s*\(/,
|
|
61
62
|
/\bremoveNewlines\s*\(/,
|
|
62
63
|
/\bsanitizeLogValue\s*\(/,
|
|
63
|
-
// Inline CRLF-stripping regex literal: .replace(/[\r\n]/g, '')
|
|
64
|
-
/\.replace\(\s*\/\[\s*\\r\s*\\n\s*\]\/[gimsu]*\s*,\s*['"`]['"`]\s*\)/,
|
|
65
|
-
// Inline CRLF sequence: .replace(/\r\n/g, '')
|
|
66
|
-
/\.replace\(\s*\/\\r\\n\/[gimsu]*\s*,\s*['"`]['"`]\s*\)/,
|
|
64
|
+
// Inline CRLF-stripping regex literal: .replace(/[\r\n]/g, '') — global required
|
|
65
|
+
/\.replace\(\s*\/\[\s*\\r\s*\\n\s*\]\/[gimsu]*g[gimsu]*\s*,\s*['"`]['"`]\s*\)/,
|
|
67
66
|
];
|
|
68
67
|
// ---------------------------------------------------------------------------
|
|
69
68
|
// Stage 17 — Python ldap_injection (CWE-90) regex-strip wrapper recognition.
|
|
@@ -291,6 +290,60 @@ function argvFirstLiteralIsShell(lineText) {
|
|
|
291
290
|
.toLowerCase();
|
|
292
291
|
return SHELL_PROGRAMS.has(base);
|
|
293
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* cognium-dev#626 — does a same-file call of the wrapper method that contains
|
|
295
|
+
* the sink pass a shell argv (`Arrays.asList("/bin/sh", "-c", ...)`,
|
|
296
|
+
* `List.of("bash", ...)`, `new String[]{"sh", ...}`) at the position of its
|
|
297
|
+
* parameter `paramName`?
|
|
298
|
+
*
|
|
299
|
+
* Reuses the #351 literal test (`argvFirstLiteralIsShell`) on the caller's
|
|
300
|
+
* argument text, so the criterion is the same one the inline form already
|
|
301
|
+
* obeys. The wrapper is the method (name and parameter list) of the declaring
|
|
302
|
+
* type that contains the sink line. A call counts only when it has no receiver
|
|
303
|
+
* or a `this` receiver, sits in that same type, and passes the same number of
|
|
304
|
+
* arguments, so a call on another object, another class's method of the same
|
|
305
|
+
* name or another overload is ignored, and so is a call inside a nested type
|
|
306
|
+
* that declares its own method of that name and arity (it calls that one, not
|
|
307
|
+
* the wrapper; `enclosing_type` is unset on the default path, so the nested
|
|
308
|
+
* type is found by its line range). A parameter that is not found answers
|
|
309
|
+
* `false` and today's suppression stands.
|
|
310
|
+
*/
|
|
311
|
+
function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, calls) {
|
|
312
|
+
if (!methodName)
|
|
313
|
+
return false;
|
|
314
|
+
for (const t of types) {
|
|
315
|
+
const m = t.methods.find(mm => mm.name === methodName && sinkLine >= mm.start_line && sinkLine <= mm.end_line);
|
|
316
|
+
if (!m)
|
|
317
|
+
continue;
|
|
318
|
+
const pos = m.parameters.findIndex(p => p.name === paramName);
|
|
319
|
+
if (pos < 0)
|
|
320
|
+
return false;
|
|
321
|
+
return calls.some(c => {
|
|
322
|
+
if (c.method_name !== methodName)
|
|
323
|
+
return false;
|
|
324
|
+
if (c.receiver != null && c.receiver !== 'this')
|
|
325
|
+
return false;
|
|
326
|
+
if (c.location.line < t.start_line || c.location.line > t.end_line)
|
|
327
|
+
return false;
|
|
328
|
+
if (c.arguments.length !== m.parameters.length)
|
|
329
|
+
return false;
|
|
330
|
+
// A call inside a nested type that declares its own `methodName` of the
|
|
331
|
+
// same arity calls that method, not this wrapper (the outer type's line
|
|
332
|
+
// span covers the nested one). A nested call with no such method still
|
|
333
|
+
// resolves to the outer wrapper and counts.
|
|
334
|
+
const shadowed = types.some(u => u !== t &&
|
|
335
|
+
u.start_line >= t.start_line && u.end_line <= t.end_line &&
|
|
336
|
+
(u.start_line > t.start_line || u.end_line < t.end_line) &&
|
|
337
|
+
c.location.line >= u.start_line && c.location.line <= u.end_line &&
|
|
338
|
+
u.methods.some(um => um.name === methodName && um.parameters.length === c.arguments.length));
|
|
339
|
+
if (shadowed)
|
|
340
|
+
return false;
|
|
341
|
+
const expr = c.arguments[pos]?.expression;
|
|
342
|
+
return typeof expr === 'string' && argvFirstLiteralIsShell(`new ProcessBuilder(${expr}`);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
294
347
|
// ---------------------------------------------------------------------------
|
|
295
348
|
// Stage 12 — Java throw-statement FP suppression.
|
|
296
349
|
// (cognium-dev #157 — Sprint 45)
|
|
@@ -464,6 +517,131 @@ const SERVER_SIGNAL_RE = /["']use server["']|\b(?:getServerSideProps|getStaticPr
|
|
|
464
517
|
// interpolated, the destination host is not attacker-controlled. Matches a
|
|
465
518
|
// backtick + scheme + a literal host run (no `${` before the first `/`).
|
|
466
519
|
const FIXED_HOST_TEMPLATE_RE = /`https?:\/\/[^/${}\s`]+[/`]/;
|
|
520
|
+
/** A bash URL word whose scheme and host are literal and closed by `/` (#495). */
|
|
521
|
+
const BASH_FIXED_HOST_URL_RE = /^(?:https?|ftp):\/\/[A-Za-z0-9.-]+(?::\d+)?\//;
|
|
522
|
+
/**
|
|
523
|
+
* True when the string literal that ended at `end` is the WHOLE initializer:
|
|
524
|
+
* the next code token is `;`, `,`, end of input, or (ASI) a token on a later
|
|
525
|
+
* line that cannot continue the expression. A same-line operator (`+`, `.`,
|
|
526
|
+
* `||` …) or a next-line `+` / `.` / `(` / `[` / template continuation means
|
|
527
|
+
* the literal is only the start of a larger expression, so it is not a
|
|
528
|
+
* constant. An optional TS `as const` / `as string` suffix is skipped (it may
|
|
529
|
+
* sit on the next line).
|
|
530
|
+
* (cognium-dev#680, #681)
|
|
531
|
+
*/
|
|
532
|
+
function literalEndsInitializer(code, end) {
|
|
533
|
+
let i = end;
|
|
534
|
+
let crossedNewline = false;
|
|
535
|
+
const asConst = /^\s+as\s+(?:const|string)\b/.exec(code.slice(i, i + 64));
|
|
536
|
+
if (asConst) {
|
|
537
|
+
i += asConst[0].length;
|
|
538
|
+
crossedNewline = asConst[0].includes('\n');
|
|
539
|
+
}
|
|
540
|
+
for (;;) {
|
|
541
|
+
while (i < code.length && /\s/.test(code[i])) {
|
|
542
|
+
if (code[i] === '\n')
|
|
543
|
+
crossedNewline = true;
|
|
544
|
+
i++;
|
|
545
|
+
}
|
|
546
|
+
if (code.startsWith('//', i)) {
|
|
547
|
+
const nl = code.indexOf('\n', i);
|
|
548
|
+
i = nl === -1 ? code.length : nl;
|
|
549
|
+
}
|
|
550
|
+
else if (code.startsWith('/*', i)) {
|
|
551
|
+
const close = code.indexOf('*/', i + 2);
|
|
552
|
+
if (close === -1)
|
|
553
|
+
return false;
|
|
554
|
+
if (code.slice(i, close).includes('\n'))
|
|
555
|
+
crossedNewline = true;
|
|
556
|
+
i = close + 2;
|
|
557
|
+
}
|
|
558
|
+
else
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
if (i >= code.length)
|
|
562
|
+
return true;
|
|
563
|
+
const c = code[i];
|
|
564
|
+
if (c === ';' || c === ',')
|
|
565
|
+
return true;
|
|
566
|
+
// ASI: a following line that begins a new statement terminates the initializer.
|
|
567
|
+
// A line that starts with `as` / `satisfies` / `in` / `instanceof` continues it.
|
|
568
|
+
if (/^(?:as|satisfies|in|instanceof)(?![\w$])/.test(code.slice(i, i + 12)))
|
|
569
|
+
return false;
|
|
570
|
+
return crossedNewline && !/[-+*/%.?:|&<>=!~^(\[`]/.test(c);
|
|
571
|
+
}
|
|
572
|
+
/** Decode `\xNN` / `\uNNNN` / `\u{…}` JS string escapes; other escapes are kept verbatim. */
|
|
573
|
+
function decodeJsHexEscapes(value) {
|
|
574
|
+
return value.replace(/\\(?:x([0-9a-fA-F]{2})|u([0-9a-fA-F]{4})|u\{([0-9a-fA-F]{1,6})\}|[\s\S])/g, (m, x, u4, ub) => {
|
|
575
|
+
const hex = x ?? u4 ?? ub;
|
|
576
|
+
if (hex === undefined)
|
|
577
|
+
return m;
|
|
578
|
+
const cp = parseInt(hex, 16);
|
|
579
|
+
return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The value of `name` when it is bound exactly once in the file, by `const`,
|
|
584
|
+
* to a plain string literal (no interpolation) that is the whole initializer,
|
|
585
|
+
* and has no other `let` / `var` / `function` binding; otherwise null.
|
|
586
|
+
* (cognium-dev#681)
|
|
587
|
+
*/
|
|
588
|
+
function constStringLiteralValue(code, name) {
|
|
589
|
+
const esc = name.replace(/\$/g, '\\$');
|
|
590
|
+
const decl = new RegExp(`(?:^|[^\\w$.])const\\s+${esc}\\s*(?::\\s*string\\s*)?=\\s*(?:'((?:\\\\.|[^'\\\\\\n])*)'|"((?:\\\\.|[^"\\\\\\n])*)"|\`([^\`$\\\\]*)\`)`, 'g');
|
|
591
|
+
const decls = [...code.matchAll(decl)];
|
|
592
|
+
if (decls.length !== 1)
|
|
593
|
+
return null;
|
|
594
|
+
if (!literalEndsInitializer(code, decls[0].index + decls[0][0].length))
|
|
595
|
+
return null;
|
|
596
|
+
const anyBinding = new RegExp(`(?:^|[^\\w$.])(?:const|let|var|function)\\s+${esc}\\b`, 'g');
|
|
597
|
+
if ([...code.matchAll(anyBinding)].length !== 1)
|
|
598
|
+
return null;
|
|
599
|
+
return decodeJsHexEscapes(decls[0][1] ?? decls[0][2] ?? decls[0][3] ?? '');
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The body of the template literal that is the first argument of the
|
|
603
|
+
* `new RegExp(` on `line` (1-indexed), or null. The literal may start on a
|
|
604
|
+
* following line, after whitespace or comments, as a formatter wraps a long
|
|
605
|
+
* constructor call; the `new RegExp(` itself must be on the sink line.
|
|
606
|
+
* (cognium-dev#717)
|
|
607
|
+
*/
|
|
608
|
+
function regExpTemplateArgument(code, lines, line) {
|
|
609
|
+
const text = lines[line - 1] ?? '';
|
|
610
|
+
let offset = 0;
|
|
611
|
+
for (let i = 0; i < line - 1; i++)
|
|
612
|
+
offset += lines[i].length + 1;
|
|
613
|
+
const argument = /^(?:\s|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*`((?:\\.|[^`\\])*)`(?=\s*[,)])/;
|
|
614
|
+
for (const start of text.matchAll(/new\s+RegExp\s*\(/g)) {
|
|
615
|
+
// The literal must be the whole first argument: `` `…` + text `` is not constant.
|
|
616
|
+
const m = argument.exec(code.slice(offset + start.index + start[0].length));
|
|
617
|
+
if (m)
|
|
618
|
+
return m[1];
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
/** A group that holds an unbounded quantifier and is itself quantified: `(a+)+`, `(x*)*`, `(\w{2,})+`, `(a+){2,}`. */
|
|
623
|
+
const NESTED_QUANTIFIER_RE = /\([^()]*(?:[+*]|\{\d+,\d*\})[^()]*\)\s*[+*{]/;
|
|
624
|
+
/** A template literal that starts with `${IDENT}/` (cognium-dev#680). */
|
|
625
|
+
const CONST_BASE_TEMPLATE_RE = /`\$\{\s*([A-Za-z_$][\w$]*)\s*\}\//g;
|
|
626
|
+
/**
|
|
627
|
+
* True when `name` is bound exactly once in the file, by `const`, to a string
|
|
628
|
+
* literal http(s) origin with a literal host, and is never reassigned.
|
|
629
|
+
*/
|
|
630
|
+
function isConstLiteralOrigin(code, name) {
|
|
631
|
+
const esc = name.replace(/\$/g, '\\$');
|
|
632
|
+
const decl = new RegExp(`(?:^|[^\\w$.])(const|let|var)\\s+${esc}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])(https?://[^/'"\`\\s$]+(?:/[^'"\`$]*)?)\\2`, 'gm');
|
|
633
|
+
const decls = [...code.matchAll(decl)];
|
|
634
|
+
if (decls.length !== 1 || decls[0][1] !== 'const')
|
|
635
|
+
return false;
|
|
636
|
+
if (!literalEndsInitializer(code, decls[0].index + decls[0][0].length))
|
|
637
|
+
return false;
|
|
638
|
+
const anyBinding = new RegExp(`(?:^|[^\\w$.])(?:const|let|var)\\s+${esc}\\b`, 'g');
|
|
639
|
+
if ([...code.matchAll(anyBinding)].length !== 1)
|
|
640
|
+
return false;
|
|
641
|
+
const reassign = new RegExp(`(?:^|[^\\w$.])${esc}\\s*(?:[-+]?=)(?!=)`, 'g');
|
|
642
|
+
const rest = code.slice(0, decls[0].index) + code.slice(decls[0].index + decls[0][0].length);
|
|
643
|
+
return !reassign.test(rest);
|
|
644
|
+
}
|
|
467
645
|
/**
|
|
468
646
|
* Returns the 1-indexed line numbers that a Java `if (x.contains("..")) throw/
|
|
469
647
|
* return` reject-guard makes path-traversal-safe (cognium-dev#269). A line is
|
|
@@ -604,8 +782,10 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
604
782
|
return covered;
|
|
605
783
|
}
|
|
606
784
|
/**
|
|
607
|
-
* Is this `plugin_param` source a map read
|
|
608
|
-
* a
|
|
785
|
+
* Is this `plugin_param` source a map read that provably yields `null` or a
|
|
786
|
+
* string literal — a key that was never written, or any key of a literal-only
|
|
787
|
+
* map (#584) — on a map whose entire contents we can see? (cognium-dev#328
|
|
788
|
+
* prerequisite.)
|
|
609
789
|
*
|
|
610
790
|
* Sound only when the map's provenance is fully known, so all three hold:
|
|
611
791
|
*
|
|
@@ -616,10 +796,11 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
616
796
|
* (b) the receiver never escapes — it is not passed as an argument to any
|
|
617
797
|
* call. `helper(m)` could `put` into it out of sight, so once it escapes
|
|
618
798
|
* the contents are no longer knowable from this file.
|
|
619
|
-
* (c) the read key is a string literal
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
799
|
+
* (c) the read key is a string literal. A matching `put` of that literal no
|
|
800
|
+
* longer keeps the source automatically: when every put value is itself a
|
|
801
|
+
* string literal (`isLiteralOnlyLocalMap`), the read is still dropped as
|
|
802
|
+
* non-tainted (#584). This gate only removes reads that provably yield
|
|
803
|
+
* `null` or a literal-only local map, never reads whose value is uncertain.
|
|
623
804
|
*
|
|
624
805
|
* Deliberately narrow: `plugin_param` only, literal keys only, and any
|
|
625
806
|
* uncertainty leaves the source untouched. Non-literal keys (`m.get(k)`) are
|
|
@@ -650,12 +831,183 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
650
831
|
if (escapeRe.test(stripped))
|
|
651
832
|
return false;
|
|
652
833
|
}
|
|
653
|
-
// (c2) was this literal key ever written?
|
|
834
|
+
// (c2) was this literal key ever written? If so, drop only a literal-only map.
|
|
654
835
|
const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`);
|
|
655
836
|
if (lines.some(l => putRe.test(l)))
|
|
837
|
+
return isLiteralOnlyLocalMap(lines, recv);
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* cognium-dev#584 (slice) — the map's whole contents are string literals.
|
|
842
|
+
*
|
|
843
|
+
* Every mutation of `recv` in the file is a `put(<key>, "<string literal>")`:
|
|
844
|
+
* no `put` of a non-literal value under any key (so no key-shuffle can place a
|
|
845
|
+
* tainted value under the key being read), and no bulk or computing mutator
|
|
846
|
+
* (`putAll`, `putIfAbsent`, `compute*`, `merge`, `replace*`) that could store
|
|
847
|
+
* a value out of sight. Combined with (a) local construction and (b) no escape
|
|
848
|
+
* in the caller, a read of such a map yields a literal and is not a source.
|
|
849
|
+
*/
|
|
850
|
+
function isLiteralOnlyLocalMap(lines, recv) {
|
|
851
|
+
const anyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(`, 'g');
|
|
852
|
+
const literalPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*(?:"(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)\\s*,\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\)`, 'g');
|
|
853
|
+
const otherMutator = new RegExp(`\\b${recv}\\s*\\.\\s*(?:putAll|putIfAbsent|compute|computeIfAbsent|computeIfPresent|merge|replace|replaceAll|entrySet|values)\\s*\\(`);
|
|
854
|
+
let puts = 0;
|
|
855
|
+
let literalPuts = 0;
|
|
856
|
+
for (const l of lines) {
|
|
857
|
+
if (otherMutator.test(l))
|
|
858
|
+
return false;
|
|
859
|
+
puts += (l.match(anyPut) ?? []).length;
|
|
860
|
+
literalPuts += (l.match(literalPut) ?? []).length;
|
|
861
|
+
}
|
|
862
|
+
return puts > 0 && puts === literalPuts;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Is this C# `Content(...)` call the controller helper writing an HTML body
|
|
866
|
+
* (cognium-dev#580)?
|
|
867
|
+
*
|
|
868
|
+
* - The helper is inherited (`ControllerBase.Content`, `PageModel.Content`),
|
|
869
|
+
* so it is called unqualified or on `this` / `base`. `note.Content(q)` is
|
|
870
|
+
* some other type's method.
|
|
871
|
+
* - The content type is argument 1. A literal or `MediaTypeNames` constant
|
|
872
|
+
* that is not HTML or a script-capable XML type serves the body as data. A computed content type is unknown and keeps the sink, as does the
|
|
873
|
+
* one-argument overload.
|
|
874
|
+
*/
|
|
875
|
+
function csharpContentServesHtml(call) {
|
|
876
|
+
if (call.receiver !== null && call.receiver !== 'this' && call.receiver !== 'base')
|
|
656
877
|
return false;
|
|
878
|
+
const contentType = call.arguments.find(a => a.position === 1)?.expression.trim();
|
|
879
|
+
if (contentType === undefined)
|
|
880
|
+
return true;
|
|
881
|
+
const literal = /^@?"([^"]*)"$/.exec(contentType);
|
|
882
|
+
// XML types (`text/xml`, `application/xml`, `image/svg+xml`, any `+xml`)
|
|
883
|
+
// render XHTML / SVG markup and run its scripts, so they count as HTML.
|
|
884
|
+
if (literal)
|
|
885
|
+
return /^\s*(?:text\/html|text\/xml|application\/xml|[\w.-]+\/[\w.-]+\+xml)\b/i.test(literal[1]);
|
|
886
|
+
const named = /^(?:System\s*\.\s*Net\s*\.\s*Mime\s*\.\s*)?MediaTypeNames\s*\.\s*(Text|Application)\s*\.\s*(\w+)$/.exec(contentType);
|
|
887
|
+
if (named)
|
|
888
|
+
return named[2] === 'Html' || named[2] === 'Xml';
|
|
657
889
|
return true;
|
|
658
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* True when an unqualified / `this.` / `base.` call to `Content` sits in a type
|
|
893
|
+
* that can inherit the MVC helper (cognium-dev#580). The C# extractor does not
|
|
894
|
+
* record base lists, so they are read from the declaration text.
|
|
895
|
+
* - a base named `Controller`, `ControllerBase` or `PageModel`: the helper;
|
|
896
|
+
* - a `partial` class: the base may be in another part, so treat as unknown and keep;
|
|
897
|
+
* - no base list, or only `I`-prefixed interface names: nothing is inherited,
|
|
898
|
+
* so `Content(...)` is the type's own method;
|
|
899
|
+
* - any other base: unknown, kept, unless the type declares its own `Content`
|
|
900
|
+
* name, which hides an inherited one.
|
|
901
|
+
* Non-class IR kinds (and a missing owner) keep the sink. A controller that
|
|
902
|
+
* declares its own `Content` overload keeps the helper call.
|
|
903
|
+
*/
|
|
904
|
+
function csharpContentCallInheritsHelper(call, types, sourceLines) {
|
|
905
|
+
const line = call.location.line;
|
|
906
|
+
let owner;
|
|
907
|
+
for (const t of types) {
|
|
908
|
+
if (line < t.start_line || line > t.end_line)
|
|
909
|
+
continue;
|
|
910
|
+
if (!owner || t.end_line - t.start_line < owner.end_line - owner.start_line)
|
|
911
|
+
owner = t;
|
|
912
|
+
}
|
|
913
|
+
if (!owner || owner.kind !== 'class')
|
|
914
|
+
return true;
|
|
915
|
+
const header = sourceLines.slice(owner.start_line - 1, owner.start_line + 4).join(' ');
|
|
916
|
+
const decl = new RegExp(`\\bclass\\s+${owner.name}\\b(?:\\s*<[^>]*>)?\\s*(?::\\s*([^{]*?))?\\s*(?:where\\b[^{]*)?\\{`).exec(header);
|
|
917
|
+
if (!decl)
|
|
918
|
+
return true;
|
|
919
|
+
const bases = (decl[1] ?? '').split(',').map(b => b.trim().replace(/<.*$/, '').split('.').pop() ?? '').filter(Boolean);
|
|
920
|
+
if (bases.some(b => b === 'Controller' || b === 'ControllerBase' || b === 'PageModel'))
|
|
921
|
+
return true;
|
|
922
|
+
// Partial types may declare their base in another part of the type — unknown, keep.
|
|
923
|
+
const classAt = header.search(/\bclass\b/);
|
|
924
|
+
if (classAt >= 0 && /\bpartial\b/.test(header.slice(0, classAt)))
|
|
925
|
+
return true;
|
|
926
|
+
if (bases.every(b => /^I[A-Z]/.test(b)))
|
|
927
|
+
return false;
|
|
928
|
+
return !owner.methods.some(m => m.name === 'Content');
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Does this Rust `.body(x)` call write a response whose builder chain declared
|
|
932
|
+
* a literal non-HTML content type (cognium-dev#478)? actix-web does not add a
|
|
933
|
+
* `Content-Type` of its own for `.body(...)`, so only an explicit
|
|
934
|
+
* `.content_type(<literal>)` earlier in the same chain proves the body is data:
|
|
935
|
+
*
|
|
936
|
+
* - a string literal `type/subtype` that is not HTML, XHTML or a
|
|
937
|
+
* script-capable XML type (`text/xml`, `application/xml`, any `+xml`);
|
|
938
|
+
* - `ContentType::plaintext()` / `json()` / `octet_stream()` /
|
|
939
|
+
* `form_url_encoded()` and the matching `mime::` constants.
|
|
940
|
+
*
|
|
941
|
+
* No content type, a computed one, `ContentType::html()` and every XML type
|
|
942
|
+
* keep the sink. When the chain sets the type more than once the last wins.
|
|
943
|
+
* Only a call made at the top level of the chain counts, so a nested
|
|
944
|
+
* `content_type(...)` inside an argument is never read as the response type.
|
|
945
|
+
*/
|
|
946
|
+
function rustBodyDeclaresNonHtmlType(call) {
|
|
947
|
+
const recv = call.receiver;
|
|
948
|
+
if (!recv)
|
|
949
|
+
return false;
|
|
950
|
+
let arg;
|
|
951
|
+
let depth = 0;
|
|
952
|
+
let quote = false;
|
|
953
|
+
for (let i = 0; i < recv.length; i++) {
|
|
954
|
+
const ch = recv[i];
|
|
955
|
+
if (quote) {
|
|
956
|
+
if (ch === '\\')
|
|
957
|
+
i++;
|
|
958
|
+
else if (ch === '"')
|
|
959
|
+
quote = false;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (ch === '"') {
|
|
963
|
+
quote = true;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
967
|
+
depth++;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
971
|
+
depth--;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (depth === 0 && recv.startsWith('.content_type(', i)) {
|
|
975
|
+
const start = i + '.content_type('.length;
|
|
976
|
+
let d = 1;
|
|
977
|
+
let q = false;
|
|
978
|
+
let j = start;
|
|
979
|
+
for (; j < recv.length && d > 0; j++) {
|
|
980
|
+
const c = recv[j];
|
|
981
|
+
if (q) {
|
|
982
|
+
if (c === '\\')
|
|
983
|
+
j++;
|
|
984
|
+
else if (c === '"')
|
|
985
|
+
q = false;
|
|
986
|
+
}
|
|
987
|
+
else if (c === '"')
|
|
988
|
+
q = true;
|
|
989
|
+
else if (c === '(')
|
|
990
|
+
d++;
|
|
991
|
+
else if (c === ')')
|
|
992
|
+
d--;
|
|
993
|
+
}
|
|
994
|
+
arg = d === 0 ? recv.slice(start, j - 1).trim() : undefined;
|
|
995
|
+
i = j - 1;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (arg === undefined)
|
|
999
|
+
return false;
|
|
1000
|
+
const literal = /^"([^"]*)"$/.exec(arg);
|
|
1001
|
+
if (literal) {
|
|
1002
|
+
const type = literal[1];
|
|
1003
|
+
if (!/^\s*[\w.-]+\/[\w.+-]+/.test(type))
|
|
1004
|
+
return false;
|
|
1005
|
+
return !/^\s*(?:text\/html|application\/xhtml\+xml|text\/xml|application\/xml|[\w.-]+\/[\w.-]+\+xml)\b/i.test(type);
|
|
1006
|
+
}
|
|
1007
|
+
if (/^(?:(?:actix_web\s*::\s*)?(?:http\s*::\s*header\s*::\s*)?)ContentType\s*::\s*(?:plaintext|json|octet_stream|form_url_encoded)\s*\(\s*\)$/.test(arg))
|
|
1008
|
+
return true;
|
|
1009
|
+
return /^(?:::)?mime\s*::\s*(?:TEXT_PLAIN|TEXT_PLAIN_UTF_8|APPLICATION_JSON|APPLICATION_OCTET_STREAM)$/.test(arg);
|
|
1010
|
+
}
|
|
659
1011
|
/**
|
|
660
1012
|
* Compute, per sink type, the set of C# variables whose value has passed through
|
|
661
1013
|
* a sanitizer for that type (cognium-dev C#/.NET Phase-1). Seeds from
|
|
@@ -669,6 +1021,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
669
1021
|
const lines = code.split('\n');
|
|
670
1022
|
const byType = new Map();
|
|
671
1023
|
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
1024
|
+
const sanitizers = csharpSanitizersFor(code);
|
|
672
1025
|
for (const { type } of CSHARP_SANITIZER_RES) {
|
|
673
1026
|
if (!byType.has(type))
|
|
674
1027
|
byType.set(type, new Set());
|
|
@@ -679,7 +1032,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
679
1032
|
if (!m)
|
|
680
1033
|
continue;
|
|
681
1034
|
const [, lhs, rhs] = m;
|
|
682
|
-
for (const { re, type } of
|
|
1035
|
+
for (const { re, type } of sanitizers) {
|
|
683
1036
|
if (re.test(rhs))
|
|
684
1037
|
byType.get(type).add(lhs);
|
|
685
1038
|
}
|
|
@@ -1039,13 +1392,26 @@ export class SinkFilterPass {
|
|
|
1039
1392
|
// That is SecuriBench Micro `Collections6:47`, an `/* OK */` line. The
|
|
1040
1393
|
// false positive is currently masked by the const-prop veto in #328, so
|
|
1041
1394
|
// this gate is behaviour-neutral today; its purpose is to make lifting that
|
|
1042
|
-
// veto safe, which is the sequencing chosen on #328.
|
|
1395
|
+
// veto safe, which is the sequencing chosen on #328. #584 widened it: a read
|
|
1396
|
+
// of a key that WAS put is also dropped when every value put on that local,
|
|
1397
|
+
// non-escaping map is a string literal (`isLiteralOnlyLocalMap`).
|
|
1043
1398
|
const sources = ctx.code
|
|
1044
1399
|
? executableSources.filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
|
|
1045
1400
|
: executableSources;
|
|
1046
1401
|
// Build merged sinks, deduplicating JS DOM sinks that may overlap with config sinks.
|
|
1047
1402
|
const sinks = [...taintMatcher.sinks];
|
|
1048
1403
|
for (const s of langSources.additionalSinks) {
|
|
1404
|
+
const springReturn = s.type === 'xss' &&
|
|
1405
|
+
typeof s.location === 'string' &&
|
|
1406
|
+
s.location.startsWith('Spring handler returns HTML');
|
|
1407
|
+
if (springReturn) {
|
|
1408
|
+
const i = sinks.findIndex(x => x.line === s.line && x.type === 'xss');
|
|
1409
|
+
if (i >= 0)
|
|
1410
|
+
sinks[i] = s;
|
|
1411
|
+
else if (!sinks.some(x => x.line === s.line && x.cwe === s.cwe && x.type === s.type))
|
|
1412
|
+
sinks.push(s);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1049
1415
|
if (!sinks.some(x => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
|
|
1050
1416
|
sinks.push(s);
|
|
1051
1417
|
}
|
|
@@ -1059,7 +1425,7 @@ export class SinkFilterPass {
|
|
|
1059
1425
|
// Stage 2 — clean array elements
|
|
1060
1426
|
filtered = filterCleanArraySinks(filtered, calls, constProp.taintedArrayElements, constProp.symbols);
|
|
1061
1427
|
// Stage 3 — clean variables
|
|
1062
|
-
filtered = filterCleanVariableSinks(filtered, calls, constProp.tainted, constProp.symbols, dfg, constProp.sanitizedVars, constProp.synchronizedLines, language);
|
|
1428
|
+
filtered = filterCleanVariableSinks(filtered, calls, constProp.tainted, constProp.symbols, dfg, constProp.sanitizedVars, constProp.synchronizedLines, language, constProp.sanitizedVarUncovered);
|
|
1063
1429
|
// Stage 4 — sanitized sinks. A sanitizer on one operand does not drop
|
|
1064
1430
|
// the sink while another source operand is still raw (#641).
|
|
1065
1431
|
filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
|
|
@@ -1582,8 +1948,12 @@ export class SinkFilterPass {
|
|
|
1582
1948
|
// Bare identifier — parameter/field lookup.
|
|
1583
1949
|
if (varName !== '' && argExpr === varName && !/[(.]/.test(argExpr)) {
|
|
1584
1950
|
const t = resolveIdentifierType(varName, ctorCall.in_method, irTypes);
|
|
1585
|
-
|
|
1951
|
+
// cognium-dev#626: a `List<String>` PARAMETER is argv-form only while no
|
|
1952
|
+
// same-file caller hands it a shell argv (`/bin/sh -c <script>`): then
|
|
1953
|
+
// the tainted element is the script, so the suppression proves nothing.
|
|
1954
|
+
if (isArgvContainerType(t) && !sameFileCallerPassesShellArgv(varName, ctorCall.in_method, ctorCall.location.line, irTypes, irCalls)) {
|
|
1586
1955
|
return false;
|
|
1956
|
+
}
|
|
1587
1957
|
}
|
|
1588
1958
|
// Method call — return-type lookup. `variable` field carries the
|
|
1589
1959
|
// callee name for call-expression args.
|
|
@@ -2042,7 +2412,51 @@ export class SinkFilterPass {
|
|
|
2042
2412
|
if (sink.type !== 'ssrf')
|
|
2043
2413
|
return true;
|
|
2044
2414
|
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
2045
|
-
|
|
2415
|
+
if (FIXED_HOST_TEMPLATE_RE.test(sinkLineText))
|
|
2416
|
+
return false;
|
|
2417
|
+
// `${API_BASE}/path` where API_BASE is bound once by `const` to a
|
|
2418
|
+
// literal origin: the authority is constant and closed by `/` (#680).
|
|
2419
|
+
for (const m of sinkLineText.matchAll(CONST_BASE_TEMPLATE_RE)) {
|
|
2420
|
+
if (isConstLiteralOrigin(ctx.code, m[1]))
|
|
2421
|
+
return false;
|
|
2422
|
+
}
|
|
2423
|
+
return true;
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
// Stage 15k — JS/TS redos: `new RegExp(`…${CONST}…`)` whose every
|
|
2427
|
+
// interpolation is a module `const` bound to a string literal has a
|
|
2428
|
+
// constant pattern; a tainted subject alone is not ReDoS. (cognium-dev#681)
|
|
2429
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
2430
|
+
const sourceLines = ctx.code.split('\n');
|
|
2431
|
+
filtered = filtered.filter(sink => {
|
|
2432
|
+
if (sink.type !== 'redos')
|
|
2433
|
+
return true;
|
|
2434
|
+
const tpl = regExpTemplateArgument(ctx.code, sourceLines, sink.line);
|
|
2435
|
+
if (tpl === null)
|
|
2436
|
+
return true;
|
|
2437
|
+
let pattern = tpl;
|
|
2438
|
+
let interpolations = 0;
|
|
2439
|
+
let allConst = true;
|
|
2440
|
+
// Replace each `${ NAME }` as written (whitespace inside the braces
|
|
2441
|
+
// included) by the const's literal value.
|
|
2442
|
+
pattern = pattern.replace(/\$\{([^}]*)\}/g, (_full, raw) => {
|
|
2443
|
+
interpolations++;
|
|
2444
|
+
const name = raw.trim();
|
|
2445
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
2446
|
+
allConst = false;
|
|
2447
|
+
return '';
|
|
2448
|
+
}
|
|
2449
|
+
const value = constStringLiteralValue(ctx.code, name);
|
|
2450
|
+
if (value === null) {
|
|
2451
|
+
allConst = false;
|
|
2452
|
+
return '';
|
|
2453
|
+
}
|
|
2454
|
+
return value;
|
|
2455
|
+
});
|
|
2456
|
+
if (interpolations === 0 || !allConst)
|
|
2457
|
+
return true;
|
|
2458
|
+
// A constant pattern can still backtrack on a tainted subject.
|
|
2459
|
+
return NESTED_QUANTIFIER_RE.test(pattern);
|
|
2046
2460
|
});
|
|
2047
2461
|
}
|
|
2048
2462
|
// Stage 15f — JS/TS ssrf: anchored host allow-list reject-guard.
|
|
@@ -2118,6 +2532,50 @@ export class SinkFilterPass {
|
|
|
2118
2532
|
return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
|
|
2119
2533
|
});
|
|
2120
2534
|
}
|
|
2535
|
+
// Stage 15k' — C# `Content(body, contentType)` serves HTML only when the
|
|
2536
|
+
// content type is HTML (cognium-dev#580). The `Content` xss sink stays when
|
|
2537
|
+
// at least one `Content` call on its line is the controller helper with an
|
|
2538
|
+
// HTML or unknown content type.
|
|
2539
|
+
if (language === 'csharp') {
|
|
2540
|
+
const csContentLines = ctx.code.split('\n');
|
|
2541
|
+
filtered = filtered.filter(sink => {
|
|
2542
|
+
if (sink.type !== 'xss' || sink.method !== 'Content')
|
|
2543
|
+
return true;
|
|
2544
|
+
const contentCalls = callsByLine.get(sink.line)?.filter(c => c.method_name === 'Content') ?? [];
|
|
2545
|
+
// The call must also sit in a type that can inherit the MVC helper (#580).
|
|
2546
|
+
return contentCalls.length === 0 || contentCalls.some(c => csharpContentCallInheritsHelper(c, ctx.graph.ir.types, csContentLines) && csharpContentServesHtml(c));
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
// Stage 15k'' — Rust `.body(x)` with a literal non-HTML content type is
|
|
2550
|
+
// data, not markup (cognium-dev#478). The xss sink stays unless every
|
|
2551
|
+
// `body` call on its line sits on a chain that declared such a type.
|
|
2552
|
+
if (language === 'rust') {
|
|
2553
|
+
filtered = filtered.filter(sink => {
|
|
2554
|
+
if (sink.type !== 'xss' || sink.method !== 'body')
|
|
2555
|
+
return true;
|
|
2556
|
+
const bodyCalls = callsByLine.get(sink.line)?.filter(c => c.method_name === 'body') ?? [];
|
|
2557
|
+
return bodyCalls.length === 0 || !bodyCalls.every(rustBodyDeclaresNonHtmlType);
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
// Stage 15m — bash ssrf: fixed-host URL word (cognium-dev#495, #669).
|
|
2561
|
+
// The bash analogue of Stage 15i: `curl -- "https://api.example.com/${p}"`
|
|
2562
|
+
// can only reach the literal host, whatever `$p` holds. Every word on the
|
|
2563
|
+
// line that expands a variable must be such a URL; a word like `"$url"`,
|
|
2564
|
+
// `"https://${host}/x"` or `"https://api.example.com${p}"` (no `/` to
|
|
2565
|
+
// close the authority, so `$p` may be `@evil.com`) keeps the sink.
|
|
2566
|
+
if (language === 'bash') {
|
|
2567
|
+
const sourceLines = ctx.code.split('\n');
|
|
2568
|
+
filtered = filtered.filter(sink => {
|
|
2569
|
+
if (sink.type !== 'ssrf')
|
|
2570
|
+
return true;
|
|
2571
|
+
const words = (sourceLines[sink.line - 1] ?? '').match(/"[^"]*"|'[^']*'|[^\s"']+/g) ?? [];
|
|
2572
|
+
const expanding = words.filter(w => !w.startsWith("'") && w.includes('$'));
|
|
2573
|
+
// Unquoted (`https://h/$p`), the expansion is word-split, so `$p`
|
|
2574
|
+
// can add a second URL or a flag: only a double-quoted word counts.
|
|
2575
|
+
return expanding.length === 0 ||
|
|
2576
|
+
!expanding.every(w => w.startsWith('"') && BASH_FIXED_HOST_URL_RE.test(w.slice(1, -1)));
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2121
2579
|
// Stage 15l — C# ssrf: HttpClient with a constant BaseAddress and a
|
|
2122
2580
|
// relative request path. (cognium-dev#286 D)
|
|
2123
2581
|
//
|
|
@@ -2779,14 +3237,13 @@ function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
|
2779
3237
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
2780
3238
|
for (const call of callsAtSink) {
|
|
2781
3239
|
for (const arg of call.arguments) {
|
|
2782
|
-
const
|
|
2783
|
-
if (
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const taintedIndices = taintedArrayElements.get(arrayName);
|
|
3240
|
+
const arrayAccess = parseJavaArrayAccessExpression(arg.expression);
|
|
3241
|
+
if (arrayAccess) {
|
|
3242
|
+
let indexKey = arrayAccess.indexKey;
|
|
3243
|
+
indexKey = evaluateSimpleExpression(indexKey, symbols);
|
|
3244
|
+
const taintedIndices = taintedArrayElements.get(arrayAccess.arrayName);
|
|
2788
3245
|
if (taintedIndices !== undefined) {
|
|
2789
|
-
const isTainted = taintedIndices.has(
|
|
3246
|
+
const isTainted = taintedIndices.has(indexKey) || taintedIndices.has('*');
|
|
2790
3247
|
if (!isTainted)
|
|
2791
3248
|
return false;
|
|
2792
3249
|
}
|
|
@@ -2796,7 +3253,7 @@ function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
|
2796
3253
|
return true;
|
|
2797
3254
|
});
|
|
2798
3255
|
}
|
|
2799
|
-
export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanitizedVars, synchronizedLines, language) {
|
|
3256
|
+
export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanitizedVars, synchronizedLines, language, sanitizedVarUncovered) {
|
|
2800
3257
|
const fieldNames = new Set();
|
|
2801
3258
|
if (dfg) {
|
|
2802
3259
|
for (const def of dfg.defs) {
|
|
@@ -2862,8 +3319,33 @@ export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg
|
|
|
2862
3319
|
argClean = false;
|
|
2863
3320
|
break;
|
|
2864
3321
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
3322
|
+
// #589 review: when any `method:var` sanitizer exists, decide from
|
|
3323
|
+
// the scoped name only — do not treat a sibling method's encoder
|
|
3324
|
+
// (or its unscoped alias) as sanitizing this method's local.
|
|
3325
|
+
const suffix = `:${varName}`;
|
|
3326
|
+
let hasScopedSanitizer = false;
|
|
3327
|
+
if (sanitizedVars) {
|
|
3328
|
+
for (const s of sanitizedVars) {
|
|
3329
|
+
if (s !== varName && s.endsWith(suffix)) {
|
|
3330
|
+
hasScopedSanitizer = true;
|
|
3331
|
+
break;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
const isSanitizedHere = sanitizedVars?.has(scopedName)
|
|
3336
|
+
|| (!hasScopedSanitizer && !!sanitizedVars?.has(varName));
|
|
3337
|
+
if (isSanitizedHere) {
|
|
3338
|
+
// #589: a context-specific encoder (`escapeHtml`, `encodeForHTML`, …)
|
|
3339
|
+
// does not clean `trust_boundary`; that sink on the same
|
|
3340
|
+
// variable stays.
|
|
3341
|
+
const uncovered = hasScopedSanitizer
|
|
3342
|
+
? sanitizedVarUncovered?.get(scopedName)
|
|
3343
|
+
: (sanitizedVarUncovered?.get(scopedName) ?? sanitizedVarUncovered?.get(varName));
|
|
3344
|
+
if (!uncovered || !uncovered.has(sink.type))
|
|
3345
|
+
continue;
|
|
3346
|
+
argClean = false;
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
2867
3349
|
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
2868
3350
|
argClean = false;
|
|
2869
3351
|
break;
|