circle-ir 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +134 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +98 -3
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +109 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1176 -119
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +307 -31
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1349 -58
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +8 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -1
- package/dist/analysis/scalar-derivation.js +172 -0
- package/dist/analysis/scalar-derivation.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +531 -31
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +234 -13
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -3
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +24319 -19934
- package/dist/core/circle-ir-core.cjs +4936 -3575
- package/dist/core/circle-ir-core.js +4936 -3575
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +102 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +102 -43
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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)
|
|
@@ -545,6 +598,27 @@ function constStringLiteralValue(code, name) {
|
|
|
545
598
|
return null;
|
|
546
599
|
return decodeJsHexEscapes(decls[0][1] ?? decls[0][2] ?? decls[0][3] ?? '');
|
|
547
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
|
+
}
|
|
548
622
|
/** A group that holds an unbounded quantifier and is itself quantified: `(a+)+`, `(x*)*`, `(\w{2,})+`, `(a+){2,}`. */
|
|
549
623
|
const NESTED_QUANTIFIER_RE = /\([^()]*(?:[+*]|\{\d+,\d*\})[^()]*\)\s*[+*{]/;
|
|
550
624
|
/** A template literal that starts with `${IDENT}/` (cognium-dev#680). */
|
|
@@ -708,8 +782,10 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
708
782
|
return covered;
|
|
709
783
|
}
|
|
710
784
|
/**
|
|
711
|
-
* Is this `plugin_param` source a map read
|
|
712
|
-
* 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.)
|
|
713
789
|
*
|
|
714
790
|
* Sound only when the map's provenance is fully known, so all three hold:
|
|
715
791
|
*
|
|
@@ -720,10 +796,11 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
720
796
|
* (b) the receiver never escapes — it is not passed as an argument to any
|
|
721
797
|
* call. `helper(m)` could `put` into it out of sight, so once it escapes
|
|
722
798
|
* the contents are no longer knowable from this file.
|
|
723
|
-
* (c) the read key is a string literal
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
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.
|
|
727
804
|
*
|
|
728
805
|
* Deliberately narrow: `plugin_param` only, literal keys only, and any
|
|
729
806
|
* uncertainty leaves the source untouched. Non-literal keys (`m.get(k)`) are
|
|
@@ -754,12 +831,36 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
754
831
|
if (escapeRe.test(stripped))
|
|
755
832
|
return false;
|
|
756
833
|
}
|
|
757
|
-
// (c2) was this literal key ever written?
|
|
834
|
+
// (c2) was this literal key ever written? If so, drop only a literal-only map.
|
|
758
835
|
const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`);
|
|
759
836
|
if (lines.some(l => putRe.test(l)))
|
|
760
|
-
return
|
|
837
|
+
return isLiteralOnlyLocalMap(lines, recv);
|
|
761
838
|
return true;
|
|
762
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
|
+
}
|
|
763
864
|
/**
|
|
764
865
|
* Is this C# `Content(...)` call the controller helper writing an HTML body
|
|
765
866
|
* (cognium-dev#580)?
|
|
@@ -787,6 +888,126 @@ function csharpContentServesHtml(call) {
|
|
|
787
888
|
return named[2] === 'Html' || named[2] === 'Xml';
|
|
788
889
|
return true;
|
|
789
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
|
+
}
|
|
790
1011
|
/**
|
|
791
1012
|
* Compute, per sink type, the set of C# variables whose value has passed through
|
|
792
1013
|
* a sanitizer for that type (cognium-dev C#/.NET Phase-1). Seeds from
|
|
@@ -800,6 +1021,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
800
1021
|
const lines = code.split('\n');
|
|
801
1022
|
const byType = new Map();
|
|
802
1023
|
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
1024
|
+
const sanitizers = csharpSanitizersFor(code);
|
|
803
1025
|
for (const { type } of CSHARP_SANITIZER_RES) {
|
|
804
1026
|
if (!byType.has(type))
|
|
805
1027
|
byType.set(type, new Set());
|
|
@@ -810,7 +1032,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
810
1032
|
if (!m)
|
|
811
1033
|
continue;
|
|
812
1034
|
const [, lhs, rhs] = m;
|
|
813
|
-
for (const { re, type } of
|
|
1035
|
+
for (const { re, type } of sanitizers) {
|
|
814
1036
|
if (re.test(rhs))
|
|
815
1037
|
byType.get(type).add(lhs);
|
|
816
1038
|
}
|
|
@@ -1170,13 +1392,26 @@ export class SinkFilterPass {
|
|
|
1170
1392
|
// That is SecuriBench Micro `Collections6:47`, an `/* OK */` line. The
|
|
1171
1393
|
// false positive is currently masked by the const-prop veto in #328, so
|
|
1172
1394
|
// this gate is behaviour-neutral today; its purpose is to make lifting that
|
|
1173
|
-
// 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`).
|
|
1174
1398
|
const sources = ctx.code
|
|
1175
1399
|
? executableSources.filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
|
|
1176
1400
|
: executableSources;
|
|
1177
1401
|
// Build merged sinks, deduplicating JS DOM sinks that may overlap with config sinks.
|
|
1178
1402
|
const sinks = [...taintMatcher.sinks];
|
|
1179
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
|
+
}
|
|
1180
1415
|
if (!sinks.some(x => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
|
|
1181
1416
|
sinks.push(s);
|
|
1182
1417
|
}
|
|
@@ -1190,7 +1425,7 @@ export class SinkFilterPass {
|
|
|
1190
1425
|
// Stage 2 — clean array elements
|
|
1191
1426
|
filtered = filterCleanArraySinks(filtered, calls, constProp.taintedArrayElements, constProp.symbols);
|
|
1192
1427
|
// Stage 3 — clean variables
|
|
1193
|
-
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);
|
|
1194
1429
|
// Stage 4 — sanitized sinks. A sanitizer on one operand does not drop
|
|
1195
1430
|
// the sink while another source operand is still raw (#641).
|
|
1196
1431
|
filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
|
|
@@ -1713,8 +1948,12 @@ export class SinkFilterPass {
|
|
|
1713
1948
|
// Bare identifier — parameter/field lookup.
|
|
1714
1949
|
if (varName !== '' && argExpr === varName && !/[(.]/.test(argExpr)) {
|
|
1715
1950
|
const t = resolveIdentifierType(varName, ctorCall.in_method, irTypes);
|
|
1716
|
-
|
|
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)) {
|
|
1717
1955
|
return false;
|
|
1956
|
+
}
|
|
1718
1957
|
}
|
|
1719
1958
|
// Method call — return-type lookup. `variable` field carries the
|
|
1720
1959
|
// callee name for call-expression args.
|
|
@@ -2192,10 +2431,10 @@ export class SinkFilterPass {
|
|
|
2192
2431
|
filtered = filtered.filter(sink => {
|
|
2193
2432
|
if (sink.type !== 'redos')
|
|
2194
2433
|
return true;
|
|
2195
|
-
const
|
|
2196
|
-
if (
|
|
2434
|
+
const tpl = regExpTemplateArgument(ctx.code, sourceLines, sink.line);
|
|
2435
|
+
if (tpl === null)
|
|
2197
2436
|
return true;
|
|
2198
|
-
let pattern =
|
|
2437
|
+
let pattern = tpl;
|
|
2199
2438
|
let interpolations = 0;
|
|
2200
2439
|
let allConst = true;
|
|
2201
2440
|
// Replace each `${ NAME }` as written (whitespace inside the braces
|
|
@@ -2298,11 +2537,24 @@ export class SinkFilterPass {
|
|
|
2298
2537
|
// at least one `Content` call on its line is the controller helper with an
|
|
2299
2538
|
// HTML or unknown content type.
|
|
2300
2539
|
if (language === 'csharp') {
|
|
2540
|
+
const csContentLines = ctx.code.split('\n');
|
|
2301
2541
|
filtered = filtered.filter(sink => {
|
|
2302
2542
|
if (sink.type !== 'xss' || sink.method !== 'Content')
|
|
2303
2543
|
return true;
|
|
2304
2544
|
const contentCalls = callsByLine.get(sink.line)?.filter(c => c.method_name === 'Content') ?? [];
|
|
2305
|
-
|
|
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);
|
|
2306
2558
|
});
|
|
2307
2559
|
}
|
|
2308
2560
|
// Stage 15m — bash ssrf: fixed-host URL word (cognium-dev#495, #669).
|
|
@@ -2985,14 +3237,13 @@ function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
|
2985
3237
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
2986
3238
|
for (const call of callsAtSink) {
|
|
2987
3239
|
for (const arg of call.arguments) {
|
|
2988
|
-
const
|
|
2989
|
-
if (
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
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);
|
|
2994
3245
|
if (taintedIndices !== undefined) {
|
|
2995
|
-
const isTainted = taintedIndices.has(
|
|
3246
|
+
const isTainted = taintedIndices.has(indexKey) || taintedIndices.has('*');
|
|
2996
3247
|
if (!isTainted)
|
|
2997
3248
|
return false;
|
|
2998
3249
|
}
|
|
@@ -3002,7 +3253,7 @@ function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
|
3002
3253
|
return true;
|
|
3003
3254
|
});
|
|
3004
3255
|
}
|
|
3005
|
-
export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanitizedVars, synchronizedLines, language) {
|
|
3256
|
+
export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanitizedVars, synchronizedLines, language, sanitizedVarUncovered) {
|
|
3006
3257
|
const fieldNames = new Set();
|
|
3007
3258
|
if (dfg) {
|
|
3008
3259
|
for (const def of dfg.defs) {
|
|
@@ -3068,8 +3319,33 @@ export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg
|
|
|
3068
3319
|
argClean = false;
|
|
3069
3320
|
break;
|
|
3070
3321
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
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
|
+
}
|
|
3073
3349
|
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
3074
3350
|
argClean = false;
|
|
3075
3351
|
break;
|