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
|
@@ -20,6 +20,7 @@ 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
|
+
import { identifierOutsideSanitizerCalls } from '../taint-propagation.js';
|
|
23
24
|
import { resolveIdentifierType, resolveCallReturnType, isArgvContainerType, } from '../arg-type-resolver.js';
|
|
24
25
|
/** Escape regex metacharacters for safe pattern building. */
|
|
25
26
|
function escapeReSf(s) {
|
|
@@ -463,6 +464,110 @@ const SERVER_SIGNAL_RE = /["']use server["']|\b(?:getServerSideProps|getStaticPr
|
|
|
463
464
|
// interpolated, the destination host is not attacker-controlled. Matches a
|
|
464
465
|
// backtick + scheme + a literal host run (no `${` before the first `/`).
|
|
465
466
|
const FIXED_HOST_TEMPLATE_RE = /`https?:\/\/[^/${}\s`]+[/`]/;
|
|
467
|
+
/** A bash URL word whose scheme and host are literal and closed by `/` (#495). */
|
|
468
|
+
const BASH_FIXED_HOST_URL_RE = /^(?:https?|ftp):\/\/[A-Za-z0-9.-]+(?::\d+)?\//;
|
|
469
|
+
/**
|
|
470
|
+
* True when the string literal that ended at `end` is the WHOLE initializer:
|
|
471
|
+
* the next code token is `;`, `,`, end of input, or (ASI) a token on a later
|
|
472
|
+
* line that cannot continue the expression. A same-line operator (`+`, `.`,
|
|
473
|
+
* `||` …) or a next-line `+` / `.` / `(` / `[` / template continuation means
|
|
474
|
+
* the literal is only the start of a larger expression, so it is not a
|
|
475
|
+
* constant. An optional TS `as const` / `as string` suffix is skipped (it may
|
|
476
|
+
* sit on the next line).
|
|
477
|
+
* (cognium-dev#680, #681)
|
|
478
|
+
*/
|
|
479
|
+
function literalEndsInitializer(code, end) {
|
|
480
|
+
let i = end;
|
|
481
|
+
let crossedNewline = false;
|
|
482
|
+
const asConst = /^\s+as\s+(?:const|string)\b/.exec(code.slice(i, i + 64));
|
|
483
|
+
if (asConst) {
|
|
484
|
+
i += asConst[0].length;
|
|
485
|
+
crossedNewline = asConst[0].includes('\n');
|
|
486
|
+
}
|
|
487
|
+
for (;;) {
|
|
488
|
+
while (i < code.length && /\s/.test(code[i])) {
|
|
489
|
+
if (code[i] === '\n')
|
|
490
|
+
crossedNewline = true;
|
|
491
|
+
i++;
|
|
492
|
+
}
|
|
493
|
+
if (code.startsWith('//', i)) {
|
|
494
|
+
const nl = code.indexOf('\n', i);
|
|
495
|
+
i = nl === -1 ? code.length : nl;
|
|
496
|
+
}
|
|
497
|
+
else if (code.startsWith('/*', i)) {
|
|
498
|
+
const close = code.indexOf('*/', i + 2);
|
|
499
|
+
if (close === -1)
|
|
500
|
+
return false;
|
|
501
|
+
if (code.slice(i, close).includes('\n'))
|
|
502
|
+
crossedNewline = true;
|
|
503
|
+
i = close + 2;
|
|
504
|
+
}
|
|
505
|
+
else
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
if (i >= code.length)
|
|
509
|
+
return true;
|
|
510
|
+
const c = code[i];
|
|
511
|
+
if (c === ';' || c === ',')
|
|
512
|
+
return true;
|
|
513
|
+
// ASI: a following line that begins a new statement terminates the initializer.
|
|
514
|
+
// A line that starts with `as` / `satisfies` / `in` / `instanceof` continues it.
|
|
515
|
+
if (/^(?:as|satisfies|in|instanceof)(?![\w$])/.test(code.slice(i, i + 12)))
|
|
516
|
+
return false;
|
|
517
|
+
return crossedNewline && !/[-+*/%.?:|&<>=!~^(\[`]/.test(c);
|
|
518
|
+
}
|
|
519
|
+
/** Decode `\xNN` / `\uNNNN` / `\u{…}` JS string escapes; other escapes are kept verbatim. */
|
|
520
|
+
function decodeJsHexEscapes(value) {
|
|
521
|
+
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) => {
|
|
522
|
+
const hex = x ?? u4 ?? ub;
|
|
523
|
+
if (hex === undefined)
|
|
524
|
+
return m;
|
|
525
|
+
const cp = parseInt(hex, 16);
|
|
526
|
+
return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The value of `name` when it is bound exactly once in the file, by `const`,
|
|
531
|
+
* to a plain string literal (no interpolation) that is the whole initializer,
|
|
532
|
+
* and has no other `let` / `var` / `function` binding; otherwise null.
|
|
533
|
+
* (cognium-dev#681)
|
|
534
|
+
*/
|
|
535
|
+
function constStringLiteralValue(code, name) {
|
|
536
|
+
const esc = name.replace(/\$/g, '\\$');
|
|
537
|
+
const decl = new RegExp(`(?:^|[^\\w$.])const\\s+${esc}\\s*(?::\\s*string\\s*)?=\\s*(?:'((?:\\\\.|[^'\\\\\\n])*)'|"((?:\\\\.|[^"\\\\\\n])*)"|\`([^\`$\\\\]*)\`)`, 'g');
|
|
538
|
+
const decls = [...code.matchAll(decl)];
|
|
539
|
+
if (decls.length !== 1)
|
|
540
|
+
return null;
|
|
541
|
+
if (!literalEndsInitializer(code, decls[0].index + decls[0][0].length))
|
|
542
|
+
return null;
|
|
543
|
+
const anyBinding = new RegExp(`(?:^|[^\\w$.])(?:const|let|var|function)\\s+${esc}\\b`, 'g');
|
|
544
|
+
if ([...code.matchAll(anyBinding)].length !== 1)
|
|
545
|
+
return null;
|
|
546
|
+
return decodeJsHexEscapes(decls[0][1] ?? decls[0][2] ?? decls[0][3] ?? '');
|
|
547
|
+
}
|
|
548
|
+
/** A group that holds an unbounded quantifier and is itself quantified: `(a+)+`, `(x*)*`, `(\w{2,})+`, `(a+){2,}`. */
|
|
549
|
+
const NESTED_QUANTIFIER_RE = /\([^()]*(?:[+*]|\{\d+,\d*\})[^()]*\)\s*[+*{]/;
|
|
550
|
+
/** A template literal that starts with `${IDENT}/` (cognium-dev#680). */
|
|
551
|
+
const CONST_BASE_TEMPLATE_RE = /`\$\{\s*([A-Za-z_$][\w$]*)\s*\}\//g;
|
|
552
|
+
/**
|
|
553
|
+
* True when `name` is bound exactly once in the file, by `const`, to a string
|
|
554
|
+
* literal http(s) origin with a literal host, and is never reassigned.
|
|
555
|
+
*/
|
|
556
|
+
function isConstLiteralOrigin(code, name) {
|
|
557
|
+
const esc = name.replace(/\$/g, '\\$');
|
|
558
|
+
const decl = new RegExp(`(?:^|[^\\w$.])(const|let|var)\\s+${esc}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])(https?://[^/'"\`\\s$]+(?:/[^'"\`$]*)?)\\2`, 'gm');
|
|
559
|
+
const decls = [...code.matchAll(decl)];
|
|
560
|
+
if (decls.length !== 1 || decls[0][1] !== 'const')
|
|
561
|
+
return false;
|
|
562
|
+
if (!literalEndsInitializer(code, decls[0].index + decls[0][0].length))
|
|
563
|
+
return false;
|
|
564
|
+
const anyBinding = new RegExp(`(?:^|[^\\w$.])(?:const|let|var)\\s+${esc}\\b`, 'g');
|
|
565
|
+
if ([...code.matchAll(anyBinding)].length !== 1)
|
|
566
|
+
return false;
|
|
567
|
+
const reassign = new RegExp(`(?:^|[^\\w$.])${esc}\\s*(?:[-+]?=)(?!=)`, 'g');
|
|
568
|
+
const rest = code.slice(0, decls[0].index) + code.slice(decls[0].index + decls[0][0].length);
|
|
569
|
+
return !reassign.test(rest);
|
|
570
|
+
}
|
|
466
571
|
/**
|
|
467
572
|
* Returns the 1-indexed line numbers that a Java `if (x.contains("..")) throw/
|
|
468
573
|
* return` reject-guard makes path-traversal-safe (cognium-dev#269). A line is
|
|
@@ -655,6 +760,33 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
655
760
|
return false;
|
|
656
761
|
return true;
|
|
657
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Is this C# `Content(...)` call the controller helper writing an HTML body
|
|
765
|
+
* (cognium-dev#580)?
|
|
766
|
+
*
|
|
767
|
+
* - The helper is inherited (`ControllerBase.Content`, `PageModel.Content`),
|
|
768
|
+
* so it is called unqualified or on `this` / `base`. `note.Content(q)` is
|
|
769
|
+
* some other type's method.
|
|
770
|
+
* - The content type is argument 1. A literal or `MediaTypeNames` constant
|
|
771
|
+
* 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
|
|
772
|
+
* one-argument overload.
|
|
773
|
+
*/
|
|
774
|
+
function csharpContentServesHtml(call) {
|
|
775
|
+
if (call.receiver !== null && call.receiver !== 'this' && call.receiver !== 'base')
|
|
776
|
+
return false;
|
|
777
|
+
const contentType = call.arguments.find(a => a.position === 1)?.expression.trim();
|
|
778
|
+
if (contentType === undefined)
|
|
779
|
+
return true;
|
|
780
|
+
const literal = /^@?"([^"]*)"$/.exec(contentType);
|
|
781
|
+
// XML types (`text/xml`, `application/xml`, `image/svg+xml`, any `+xml`)
|
|
782
|
+
// render XHTML / SVG markup and run its scripts, so they count as HTML.
|
|
783
|
+
if (literal)
|
|
784
|
+
return /^\s*(?:text\/html|text\/xml|application\/xml|[\w.-]+\/[\w.-]+\+xml)\b/i.test(literal[1]);
|
|
785
|
+
const named = /^(?:System\s*\.\s*Net\s*\.\s*Mime\s*\.\s*)?MediaTypeNames\s*\.\s*(Text|Application)\s*\.\s*(\w+)$/.exec(contentType);
|
|
786
|
+
if (named)
|
|
787
|
+
return named[2] === 'Html' || named[2] === 'Xml';
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
658
790
|
/**
|
|
659
791
|
* Compute, per sink type, the set of C# variables whose value has passed through
|
|
660
792
|
* a sanitizer for that type (cognium-dev C#/.NET Phase-1). Seeds from
|
|
@@ -1059,8 +1191,9 @@ export class SinkFilterPass {
|
|
|
1059
1191
|
filtered = filterCleanArraySinks(filtered, calls, constProp.taintedArrayElements, constProp.symbols);
|
|
1060
1192
|
// Stage 3 — clean variables
|
|
1061
1193
|
filtered = filterCleanVariableSinks(filtered, calls, constProp.tainted, constProp.symbols, dfg, constProp.sanitizedVars, constProp.synchronizedLines, language);
|
|
1062
|
-
// Stage 4 — sanitized sinks
|
|
1063
|
-
|
|
1194
|
+
// Stage 4 — sanitized sinks. A sanitizer on one operand does not drop
|
|
1195
|
+
// the sink while another source operand is still raw (#641).
|
|
1196
|
+
filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
|
|
1064
1197
|
// Stage 5 — Python XPath FP reduction
|
|
1065
1198
|
if (language === 'python') {
|
|
1066
1199
|
const { pyTaintedVars, pySanitizedVars } = langSources;
|
|
@@ -1166,9 +1299,17 @@ export class SinkFilterPass {
|
|
|
1166
1299
|
if (sink.type !== 'xss')
|
|
1167
1300
|
return true;
|
|
1168
1301
|
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1169
|
-
// 6a. If a sanitizer is used on this line, suppress the finding
|
|
1170
|
-
|
|
1171
|
-
|
|
1302
|
+
// 6a. If a sanitizer is used on this line, suppress the finding.
|
|
1303
|
+
// The sanitizer covers its own operand. A source operand that is
|
|
1304
|
+
// still outside that call keeps the sink (#641).
|
|
1305
|
+
if (JS_XSS_SANITIZERS.some(p => p.test(sinkLineText))) {
|
|
1306
|
+
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
1307
|
+
const covering = sanitizers.filter(s => s.line === sink.line && sanitizerCoversSink(s, sink.type));
|
|
1308
|
+
const rawOperand = sources.some(s => !!s.variable &&
|
|
1309
|
+
identifierOutsideSanitizerCalls(s.variable, callsAtSink, covering, sink.type));
|
|
1310
|
+
if (!rawOperand)
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1172
1313
|
// 6b. If the RHS is a pure string literal, suppress (e.g., `.innerHTML = "<div>Hello</div>"`)
|
|
1173
1314
|
// Match: `.innerHTML = "..."` or `.innerHTML = '...'` or `.innerHTML = `...``
|
|
1174
1315
|
const assignmentMatch = sinkLineText.match(/\.(?:innerHTML|outerHTML)\s*=\s*(.+)/);
|
|
@@ -2032,7 +2173,51 @@ export class SinkFilterPass {
|
|
|
2032
2173
|
if (sink.type !== 'ssrf')
|
|
2033
2174
|
return true;
|
|
2034
2175
|
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
2035
|
-
|
|
2176
|
+
if (FIXED_HOST_TEMPLATE_RE.test(sinkLineText))
|
|
2177
|
+
return false;
|
|
2178
|
+
// `${API_BASE}/path` where API_BASE is bound once by `const` to a
|
|
2179
|
+
// literal origin: the authority is constant and closed by `/` (#680).
|
|
2180
|
+
for (const m of sinkLineText.matchAll(CONST_BASE_TEMPLATE_RE)) {
|
|
2181
|
+
if (isConstLiteralOrigin(ctx.code, m[1]))
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
return true;
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
// Stage 15k — JS/TS redos: `new RegExp(`…${CONST}…`)` whose every
|
|
2188
|
+
// interpolation is a module `const` bound to a string literal has a
|
|
2189
|
+
// constant pattern; a tainted subject alone is not ReDoS. (cognium-dev#681)
|
|
2190
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
2191
|
+
const sourceLines = ctx.code.split('\n');
|
|
2192
|
+
filtered = filtered.filter(sink => {
|
|
2193
|
+
if (sink.type !== 'redos')
|
|
2194
|
+
return true;
|
|
2195
|
+
const m = (sourceLines[sink.line - 1] ?? '').match(/new\s+RegExp\s*\(\s*`((?:\\.|[^`\\])*)`/);
|
|
2196
|
+
if (!m)
|
|
2197
|
+
return true;
|
|
2198
|
+
let pattern = m[1];
|
|
2199
|
+
let interpolations = 0;
|
|
2200
|
+
let allConst = true;
|
|
2201
|
+
// Replace each `${ NAME }` as written (whitespace inside the braces
|
|
2202
|
+
// included) by the const's literal value.
|
|
2203
|
+
pattern = pattern.replace(/\$\{([^}]*)\}/g, (_full, raw) => {
|
|
2204
|
+
interpolations++;
|
|
2205
|
+
const name = raw.trim();
|
|
2206
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
2207
|
+
allConst = false;
|
|
2208
|
+
return '';
|
|
2209
|
+
}
|
|
2210
|
+
const value = constStringLiteralValue(ctx.code, name);
|
|
2211
|
+
if (value === null) {
|
|
2212
|
+
allConst = false;
|
|
2213
|
+
return '';
|
|
2214
|
+
}
|
|
2215
|
+
return value;
|
|
2216
|
+
});
|
|
2217
|
+
if (interpolations === 0 || !allConst)
|
|
2218
|
+
return true;
|
|
2219
|
+
// A constant pattern can still backtrack on a tainted subject.
|
|
2220
|
+
return NESTED_QUANTIFIER_RE.test(pattern);
|
|
2036
2221
|
});
|
|
2037
2222
|
}
|
|
2038
2223
|
// Stage 15f — JS/TS ssrf: anchored host allow-list reject-guard.
|
|
@@ -2108,6 +2293,37 @@ export class SinkFilterPass {
|
|
|
2108
2293
|
return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
|
|
2109
2294
|
});
|
|
2110
2295
|
}
|
|
2296
|
+
// Stage 15k' — C# `Content(body, contentType)` serves HTML only when the
|
|
2297
|
+
// content type is HTML (cognium-dev#580). The `Content` xss sink stays when
|
|
2298
|
+
// at least one `Content` call on its line is the controller helper with an
|
|
2299
|
+
// HTML or unknown content type.
|
|
2300
|
+
if (language === 'csharp') {
|
|
2301
|
+
filtered = filtered.filter(sink => {
|
|
2302
|
+
if (sink.type !== 'xss' || sink.method !== 'Content')
|
|
2303
|
+
return true;
|
|
2304
|
+
const contentCalls = callsByLine.get(sink.line)?.filter(c => c.method_name === 'Content') ?? [];
|
|
2305
|
+
return contentCalls.length === 0 || contentCalls.some(csharpContentServesHtml);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
// Stage 15m — bash ssrf: fixed-host URL word (cognium-dev#495, #669).
|
|
2309
|
+
// The bash analogue of Stage 15i: `curl -- "https://api.example.com/${p}"`
|
|
2310
|
+
// can only reach the literal host, whatever `$p` holds. Every word on the
|
|
2311
|
+
// line that expands a variable must be such a URL; a word like `"$url"`,
|
|
2312
|
+
// `"https://${host}/x"` or `"https://api.example.com${p}"` (no `/` to
|
|
2313
|
+
// close the authority, so `$p` may be `@evil.com`) keeps the sink.
|
|
2314
|
+
if (language === 'bash') {
|
|
2315
|
+
const sourceLines = ctx.code.split('\n');
|
|
2316
|
+
filtered = filtered.filter(sink => {
|
|
2317
|
+
if (sink.type !== 'ssrf')
|
|
2318
|
+
return true;
|
|
2319
|
+
const words = (sourceLines[sink.line - 1] ?? '').match(/"[^"]*"|'[^']*'|[^\s"']+/g) ?? [];
|
|
2320
|
+
const expanding = words.filter(w => !w.startsWith("'") && w.includes('$'));
|
|
2321
|
+
// Unquoted (`https://h/$p`), the expansion is word-split, so `$p`
|
|
2322
|
+
// can add a second URL or a flag: only a double-quoted word counts.
|
|
2323
|
+
return expanding.length === 0 ||
|
|
2324
|
+
!expanding.every(w => w.startsWith('"') && BASH_FIXED_HOST_URL_RE.test(w.slice(1, -1)));
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2111
2327
|
// Stage 15l — C# ssrf: HttpClient with a constant BaseAddress and a
|
|
2112
2328
|
// relative request path. (cognium-dev#286 D)
|
|
2113
2329
|
//
|
|
@@ -2881,9 +3097,14 @@ export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg
|
|
|
2881
3097
|
return true;
|
|
2882
3098
|
});
|
|
2883
3099
|
}
|
|
2884
|
-
export function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
3100
|
+
export function filterSanitizedSinks(sinks, sanitizers, calls, sources) {
|
|
2885
3101
|
if (!sanitizers || sanitizers.length === 0)
|
|
2886
3102
|
return sinks;
|
|
3103
|
+
const sourceVars = [];
|
|
3104
|
+
for (const source of sources ?? []) {
|
|
3105
|
+
if (source.variable && /^[A-Za-z_$][\w$]*$/.test(source.variable))
|
|
3106
|
+
sourceVars.push(source.variable);
|
|
3107
|
+
}
|
|
2887
3108
|
const sanitizersByLine = new Map();
|
|
2888
3109
|
for (const san of sanitizers) {
|
|
2889
3110
|
const existing = sanitizersByLine.get(san.line) ?? [];
|
|
@@ -2900,24 +3121,33 @@ export function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
2900
3121
|
const lineSanitizers = sanitizersByLine.get(sink.line);
|
|
2901
3122
|
if (!lineSanitizers || lineSanitizers.length === 0)
|
|
2902
3123
|
return true;
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3124
|
+
const lineCalls = callsByLine.get(sink.line) ?? [];
|
|
3125
|
+
const covering = lineSanitizers.filter(san => sanitizerCoversSink(san, sink.type));
|
|
3126
|
+
if (covering.length === 0)
|
|
3127
|
+
return true;
|
|
3128
|
+
// A raw source operand keeps the sink (#641). The fully wrapped twin,
|
|
3129
|
+
// and a sanitizer call with no known source operand, still drops it.
|
|
3130
|
+
for (const variable of sourceVars) {
|
|
3131
|
+
if (identifierOutsideSanitizerCalls(variable, lineCalls, covering, sink.type))
|
|
3132
|
+
return true;
|
|
3133
|
+
}
|
|
3134
|
+
for (const san of covering) {
|
|
3135
|
+
const sanMethodMatch = san.method.match(/(?:(\w+)\.)?(\w+)\(\)/);
|
|
3136
|
+
if (!sanMethodMatch)
|
|
3137
|
+
continue;
|
|
3138
|
+
const sanMethodName = sanMethodMatch[2];
|
|
3139
|
+
const sanClassName = sanMethodMatch[1];
|
|
3140
|
+
for (const call of lineCalls) {
|
|
3141
|
+
if (call.method_name === sanMethodName)
|
|
3142
|
+
continue;
|
|
3143
|
+
for (const arg of call.arguments) {
|
|
3144
|
+
const expr = arg.expression || '';
|
|
3145
|
+
if (sanClassName) {
|
|
3146
|
+
if (expr.includes(`${sanClassName}.${sanMethodName}(`))
|
|
3147
|
+
return false;
|
|
3148
|
+
}
|
|
3149
|
+
else if (expr.includes(`${sanMethodName}(`)) {
|
|
3150
|
+
return false;
|
|
2921
3151
|
}
|
|
2922
3152
|
}
|
|
2923
3153
|
}
|