circle-ir 4.9.12 → 4.9.14
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 +25 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/blocking-main-thread-pass.d.ts +19 -0
- package/dist/analysis/passes/blocking-main-thread-pass.d.ts.map +1 -1
- package/dist/analysis/passes/blocking-main-thread-pass.js +75 -2
- package/dist/analysis/passes/blocking-main-thread-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +219 -18
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +225 -4
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts +48 -0
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +166 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +17 -2
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +405 -34
- package/dist/core/circle-ir-core.cjs +60 -6
- package/dist/core/circle-ir-core.js +60 -6
- package/dist/core/extractors/calls.js +19 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +78 -1
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAo+B9E,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,sBAAsB;IACtB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AA6BD,qBAAa,cAAe,YAAW,YAAY,CAAC,gBAAgB,CAAC;IACnE,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB;CAkwCxC;AAkRD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,KAAK,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA4I1G,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,EACrB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,CAAC,EAAE,MAAM,GAChB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA6E5B;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,EAC3C,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GACvB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA2C5B"}
|
|
@@ -218,6 +218,47 @@ const OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executo
|
|
|
218
218
|
// new ProcessBuilder(new String[]{ ... })
|
|
219
219
|
// new ProcessBuilder("...", ...) (varargs ≥2 args; first is string literal)
|
|
220
220
|
const PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
|
|
221
|
+
// Same argv proof, for the `pb.command(...)` setter rather than the ctor.
|
|
222
|
+
// `command(List<String>)` / `command(String...)` feed the same argv array, so
|
|
223
|
+
// a tainted element is one argv slot, not a shell string. Added with #351:
|
|
224
|
+
// that issue widened the `command` sink's `arg_positions` beyond `[0]`, which
|
|
225
|
+
// without this would report `pb.command("ls", userInput)` as CWE-78 when it is
|
|
226
|
+
// at most argument injection (CWE-88) — exactly the FP class #179 removed for
|
|
227
|
+
// the constructor.
|
|
228
|
+
const PROCESS_BUILDER_COMMAND_ARGV_FORM_RE = /\.\s*command\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
|
|
229
|
+
// cognium-dev#351 — the argv proof above holds only while argv[0] is the
|
|
230
|
+
// PROGRAM. When argv[0] is a shell, argv[1..] is a script and a tainted
|
|
231
|
+
// element is a shell command string, so bypassing the shell is exactly what
|
|
232
|
+
// does NOT happen:
|
|
233
|
+
//
|
|
234
|
+
// new ProcessBuilder("bash", "-c", "cowsay '" + input + "'") // CWE-78
|
|
235
|
+
// new ProcessBuilder(List.of("sh", "-c", tainted)) // CWE-78
|
|
236
|
+
//
|
|
237
|
+
// `Runtime.exec(String[])` with the identical array already reported, so the
|
|
238
|
+
// argv-form suppression was the only thing standing between this shape and a
|
|
239
|
+
// finding. Kept deliberately narrow: only a literal, recognisably-shell
|
|
240
|
+
// argv[0] re-opens the sink; anything else keeps #179's suppression.
|
|
241
|
+
const SHELL_PROGRAMS = new Set([
|
|
242
|
+
'sh', 'bash', 'zsh', 'ksh', 'dash', 'ash', 'csh', 'tcsh', 'fish',
|
|
243
|
+
'cmd', 'command', 'powershell', 'pwsh',
|
|
244
|
+
]);
|
|
245
|
+
const ARGV_FIRST_LITERAL_RE = /(?:\bnew\s+ProcessBuilder\s*\(|\.\s*command\s*\()\s*(?:Arrays\.asList\s*\(|List\.of\s*\(|Collections\.singletonList\s*\(|new\s+ArrayList\s*(?:<[^>]*>)?\s*\(\s*(?:Arrays\.asList\s*\()?|new\s+String\s*\[\s*\]\s*\{)?\s*"([^"]*)"/;
|
|
246
|
+
/**
|
|
247
|
+
* True when the first argv literal on the line names a shell interpreter.
|
|
248
|
+
* Compares the basename so `/bin/bash` and `C:\\Windows\\System32\\cmd.exe`
|
|
249
|
+
* are recognised as well as a bare `bash`.
|
|
250
|
+
*/
|
|
251
|
+
function argvFirstLiteralIsShell(lineText) {
|
|
252
|
+
const m = ARGV_FIRST_LITERAL_RE.exec(lineText);
|
|
253
|
+
if (!m)
|
|
254
|
+
return false;
|
|
255
|
+
const base = m[1]
|
|
256
|
+
.split(/[\\/]/)
|
|
257
|
+
.pop()
|
|
258
|
+
.replace(/\.exe$/i, '')
|
|
259
|
+
.toLowerCase();
|
|
260
|
+
return SHELL_PROGRAMS.has(base);
|
|
261
|
+
}
|
|
221
262
|
// ---------------------------------------------------------------------------
|
|
222
263
|
// Stage 12 — Java throw-statement FP suppression.
|
|
223
264
|
// (cognium-dev #157 — Sprint 45)
|
|
@@ -533,10 +574,94 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
533
574
|
// C# sanitizers (Phase-1) and the sink types each neutralises. A tainted value
|
|
534
575
|
// that flows through one of these before reaching a sink of the matching type
|
|
535
576
|
// is safe. Method names are C#-distinctive (see config-loader C# sanitizers).
|
|
577
|
+
/**
|
|
578
|
+
* `Regex.Replace(x, "[^<allowlist>]", "")` — an allowlist character strip
|
|
579
|
+
* (cognium-dev#272, the LDAP-strip cluster).
|
|
580
|
+
*
|
|
581
|
+
* A **negated** class is the whole point: `[^a-zA-Z0-9]` deletes everything
|
|
582
|
+
* *not* in the set, so the surviving characters are exactly the set. That makes
|
|
583
|
+
* the result provably inert for the metacharacter-driven injection families
|
|
584
|
+
* below. A blacklist replace (`"[<>]"`) proves nothing — it enumerates what to
|
|
585
|
+
* remove and silently misses the rest — and must not match.
|
|
586
|
+
*
|
|
587
|
+
* Two conditions, both load-bearing:
|
|
588
|
+
*
|
|
589
|
+
* (a) the pattern is a string literal beginning `[^`. A computed pattern could
|
|
590
|
+
* be anything, including attacker-influenced.
|
|
591
|
+
* (b) the surviving set contains **no metacharacters**. `[^a-zA-Z0-9']` would
|
|
592
|
+
* keep the single quote alive, which breaks SQL and LDAP escaping outright
|
|
593
|
+
* — so the class body is restricted to alphanumerics, `\w`, `\d`, `\s`,
|
|
594
|
+
* underscore, hyphen and spaces. Anything else (quote, paren, backslash,
|
|
595
|
+
* dot, asterisk, semicolon) fails the match and the sink still fires.
|
|
596
|
+
*
|
|
597
|
+
* The replacement must be empty or a single safe character, so
|
|
598
|
+
* `Regex.Replace(x, "[^a-z]", "';DROP")` cannot qualify.
|
|
599
|
+
*/
|
|
600
|
+
const CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
|
|
601
|
+
/**
|
|
602
|
+
* Is this `plugin_param` source a map read of a key that was never written, on
|
|
603
|
+
* a map whose entire contents we can see? (cognium-dev#328 prerequisite.)
|
|
604
|
+
*
|
|
605
|
+
* Sound only when the map's provenance is fully known, so all three hold:
|
|
606
|
+
*
|
|
607
|
+
* (a) the receiver is constructed locally — `new HashMap<…>()` and friends. A
|
|
608
|
+
* map arriving as a parameter, field or return value may already hold
|
|
609
|
+
* anything, which is the case the `plugin_param` registration exists for,
|
|
610
|
+
* so it keeps its source.
|
|
611
|
+
* (b) the receiver never escapes — it is not passed as an argument to any
|
|
612
|
+
* call. `helper(m)` could `put` into it out of sight, so once it escapes
|
|
613
|
+
* the contents are no longer knowable from this file.
|
|
614
|
+
* (c) the read key is a string literal with **no** `put` of that same literal
|
|
615
|
+
* anywhere in the file. A matching `put` keeps the source regardless of
|
|
616
|
+
* whether the stored value looks tainted — this gate only removes reads
|
|
617
|
+
* that provably yield `null`, never reads whose value it judged clean.
|
|
618
|
+
*
|
|
619
|
+
* Deliberately narrow: `plugin_param` only, literal keys only, and any
|
|
620
|
+
* uncertainty leaves the source untouched. Non-literal keys (`m.get(k)`) are
|
|
621
|
+
* out of scope because the key is unknown, not because it is safe.
|
|
622
|
+
*/
|
|
623
|
+
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
624
|
+
if (source.type !== 'plugin_param')
|
|
625
|
+
return false;
|
|
626
|
+
const lines = code.split('\n');
|
|
627
|
+
const lineText = lines[source.line - 1] ?? '';
|
|
628
|
+
// (c1) the read itself: `<recv>.get("<key>")`
|
|
629
|
+
const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(\s*"([^"]*)"\s*\)/.exec(lineText);
|
|
630
|
+
if (!read)
|
|
631
|
+
return false;
|
|
632
|
+
const [, recv, key] = read;
|
|
633
|
+
// (a) locally constructed map
|
|
634
|
+
const ctorRe = new RegExp(`\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
|
|
635
|
+
if (!lines.some(l => ctorRe.test(l)))
|
|
636
|
+
return false;
|
|
637
|
+
// (b) no escape: the receiver must not appear inside any call's arguments.
|
|
638
|
+
// `m.put(...)` / `m.get(...)` are receiver positions, not arguments.
|
|
639
|
+
const escapeRe = new RegExp(`\\w\\s*\\([^)]*(?<![\\w.])${recv}(?![\\w.])[^)]*\\)`);
|
|
640
|
+
for (const l of lines) {
|
|
641
|
+
if (!escapeRe.test(l))
|
|
642
|
+
continue;
|
|
643
|
+
// Allow the map's own keyed calls on this line.
|
|
644
|
+
const stripped = l.replace(new RegExp(`\\b${recv}\\s*\\.\\s*\\w+\\s*\\(`, 'g'), 'X(');
|
|
645
|
+
if (escapeRe.test(stripped))
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
// (c2) was this literal key ever written?
|
|
649
|
+
const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`);
|
|
650
|
+
if (lines.some(l => putRe.test(l)))
|
|
651
|
+
return false;
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
536
654
|
const CSHARP_SANITIZER_RES = [
|
|
537
655
|
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: 'xss' },
|
|
538
656
|
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: 'xss' },
|
|
539
657
|
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: 'path_traversal' },
|
|
658
|
+
// An allowlist strip leaves only inert characters, so it covers every family
|
|
659
|
+
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
660
|
+
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
661
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: 'ldap_injection' },
|
|
662
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: 'xpath_injection' },
|
|
663
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: 'command_injection' },
|
|
664
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: 'sql_injection' },
|
|
540
665
|
];
|
|
541
666
|
/**
|
|
542
667
|
* Compute, per sink type, the set of C# variables whose value has passed through
|
|
@@ -903,9 +1028,28 @@ export class SinkFilterPass {
|
|
|
903
1028
|
// `ctx.language` aren't supplied (legacy callers) — this preserves
|
|
904
1029
|
// pre-3.168 behaviour for programmatic callers that pass an
|
|
905
1030
|
// already-computed source list without file text.
|
|
906
|
-
const
|
|
1031
|
+
const executableSources = (ctx.code && ctx.language)
|
|
907
1032
|
? mergedSources.filter(s => !isNonExecutableSourceLine(ctx.code, s.line, ctx.language))
|
|
908
1033
|
: mergedSources;
|
|
1034
|
+
// cognium-dev#328 prerequisite — key-aware map reads.
|
|
1035
|
+
//
|
|
1036
|
+
// `Map.get` / `HashMap.get` / … are registered as unconditional
|
|
1037
|
+
// `plugin_param` sources with `return_tainted: true` (config-loader.ts),
|
|
1038
|
+
// which models reading a config/plugin parameter map populated from
|
|
1039
|
+
// outside. Applied to *every* map, it also makes a read of a key that was
|
|
1040
|
+
// never written a source:
|
|
1041
|
+
//
|
|
1042
|
+
// Map m = new HashMap();
|
|
1043
|
+
// m.put("a", req.getParameter("name"));
|
|
1044
|
+
// String s1 = (String) m.get("b"); // <- source today; yields null
|
|
1045
|
+
//
|
|
1046
|
+
// That is SecuriBench Micro `Collections6:47`, an `/* OK */` line. The
|
|
1047
|
+
// false positive is currently masked by the const-prop veto in #328, so
|
|
1048
|
+
// this gate is behaviour-neutral today; its purpose is to make lifting that
|
|
1049
|
+
// veto safe, which is the sequencing chosen on #328.
|
|
1050
|
+
const sources = ctx.code
|
|
1051
|
+
? executableSources.filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
|
|
1052
|
+
: executableSources;
|
|
909
1053
|
// Build merged sinks, deduplicating JS DOM sinks that may overlap with config sinks.
|
|
910
1054
|
const sinks = [...taintMatcher.sinks];
|
|
911
1055
|
for (const s of langSources.additionalSinks) {
|
|
@@ -1394,13 +1538,27 @@ export class SinkFilterPass {
|
|
|
1394
1538
|
filtered = filtered.filter(sink => {
|
|
1395
1539
|
if (sink.type !== 'command_injection')
|
|
1396
1540
|
return true;
|
|
1397
|
-
if (sink.method !== 'ProcessBuilder' &&
|
|
1541
|
+
if (sink.method !== 'ProcessBuilder' &&
|
|
1542
|
+
sink.method !== 'start' &&
|
|
1543
|
+
sink.method !== 'command') {
|
|
1398
1544
|
return true;
|
|
1545
|
+
}
|
|
1399
1546
|
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1400
|
-
|
|
1547
|
+
const isCtorLine = /\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText);
|
|
1548
|
+
const isCommandLine = /\.\s*command\s*\(/.test(sinkLineText);
|
|
1549
|
+
if (!isCtorLine && !isCommandLine)
|
|
1550
|
+
return true;
|
|
1551
|
+
// #351 — a shell argv[0] means the tainted element IS a shell command
|
|
1552
|
+
// string, so the argv form proves nothing. Keep the sink.
|
|
1553
|
+
if (argvFirstLiteralIsShell(sinkLineText))
|
|
1401
1554
|
return true;
|
|
1402
|
-
if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText))
|
|
1555
|
+
if (isCtorLine && PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText))
|
|
1403
1556
|
return false;
|
|
1557
|
+
if (isCommandLine && PROCESS_BUILDER_COMMAND_ARGV_FORM_RE.test(sinkLineText)) {
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
if (!isCtorLine)
|
|
1561
|
+
return true;
|
|
1404
1562
|
// cognium-dev #256 (3.176.0): resolver fallback for method-call and
|
|
1405
1563
|
// variable arguments whose declared type is `List<String>` /
|
|
1406
1564
|
// `String[]` / etc. The regex above catches literal shapes
|
|
@@ -1912,6 +2070,69 @@ export class SinkFilterPass {
|
|
|
1912
2070
|
return true;
|
|
1913
2071
|
});
|
|
1914
2072
|
}
|
|
2073
|
+
// Stage 15l — C# ssrf: HttpClient with a constant BaseAddress and a
|
|
2074
|
+
// relative request path. (cognium-dev#286 D)
|
|
2075
|
+
//
|
|
2076
|
+
// The C# analogue of Stage 15i above (JS fixed-host URL template): when the
|
|
2077
|
+
// host is a compile-time constant, attacker input can only extend the path,
|
|
2078
|
+
// so the request cannot be redirected to another host.
|
|
2079
|
+
//
|
|
2080
|
+
// var client = new HttpClient { BaseAddress = new Uri("https://api.internal/") };
|
|
2081
|
+
// await client.GetAsync("lookup/" + Uri.EscapeDataString(input)); // not ssrf
|
|
2082
|
+
//
|
|
2083
|
+
// Three conditions, and each one is load-bearing:
|
|
2084
|
+
//
|
|
2085
|
+
// (a) `BaseAddress` is assigned a *string-literal* Uri. `new Uri(input)`
|
|
2086
|
+
// means the attacker chooses the host, which is a worse bug than the
|
|
2087
|
+
// one being suppressed here — it must keep firing.
|
|
2088
|
+
// (b) the sink call's receiver is that same client variable.
|
|
2089
|
+
// (c) the argument starts with a *string literal* that is neither absolute
|
|
2090
|
+
// (`http://`, `https://`) nor protocol-relative (`//host`). This is
|
|
2091
|
+
// the subtle one: .NET **ignores BaseAddress entirely when the request
|
|
2092
|
+
// URI is absolute**, so `client.GetAsync(input)` with a bare tainted
|
|
2093
|
+
// variable is still a genuine SSRF — the attacker supplies
|
|
2094
|
+
// `https://evil.example` and the base is discarded. Only a literal
|
|
2095
|
+
// relative prefix proves the attacker cannot reach the host position,
|
|
2096
|
+
// the same reasoning as the same-origin literal prefix in #284 d1.
|
|
2097
|
+
if (language === 'csharp') {
|
|
2098
|
+
const sourceLines = ctx.code.split('\n');
|
|
2099
|
+
// (a) client variables whose BaseAddress is a constant Uri.
|
|
2100
|
+
const constBaseClients = new Set();
|
|
2101
|
+
const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*\{[^}]*\bBaseAddress\s*=\s*new\s+Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
|
|
2102
|
+
for (const line of sourceLines) {
|
|
2103
|
+
const m = declRe.exec(line);
|
|
2104
|
+
if (m)
|
|
2105
|
+
constBaseClients.add(m[1]);
|
|
2106
|
+
}
|
|
2107
|
+
if (constBaseClients.size > 0) {
|
|
2108
|
+
// (c) the argument's leading string literal must be a relative path.
|
|
2109
|
+
const relativeLiteralArgRe = /\.\s*(?:Get|Post|Put|Patch|Delete|Send|GetString|GetByteArray|GetStream)\w*Async\s*\(\s*(?:@?\$?)"([^"]*)"/;
|
|
2110
|
+
filtered = filtered.filter(sink => {
|
|
2111
|
+
if (sink.type !== 'ssrf')
|
|
2112
|
+
return true;
|
|
2113
|
+
const text = sourceLines[sink.line - 1] ?? '';
|
|
2114
|
+
// (b) receiver must be a constant-base client.
|
|
2115
|
+
let onConstClient = false;
|
|
2116
|
+
for (const v of constBaseClients) {
|
|
2117
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
|
|
2118
|
+
onConstClient = true;
|
|
2119
|
+
break;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (!onConstClient)
|
|
2123
|
+
return true;
|
|
2124
|
+
const m = relativeLiteralArgRe.exec(text);
|
|
2125
|
+
if (!m)
|
|
2126
|
+
return true; // no leading literal — keep
|
|
2127
|
+
const lit = m[1];
|
|
2128
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit))
|
|
2129
|
+
return true; // absolute URI
|
|
2130
|
+
if (lit.startsWith('//'))
|
|
2131
|
+
return true; // protocol-relative
|
|
2132
|
+
return false; // fixed host, relative path
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
1915
2136
|
// Stage 16 — JS log_injection (CWE-117) sanitizer suppression.
|
|
1916
2137
|
// (cognium-dev #216 sanitizer-wrapped FP — Sprint 52)
|
|
1917
2138
|
//
|