circle-ir 4.9.27 → 4.9.28

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.
@@ -506,7 +506,7 @@ export class LanguageSourcesPass {
506
506
  // guard (the canonicalize-then-contain path-traversal defence). Java and
507
507
  // JS/TS already credit their equivalents above; C# was the gap.
508
508
  if (language === 'csharp') {
509
- additionalSanitizers.push(...findCSharpFullPathStartsWithGuardSanitizers(code));
509
+ additionalSanitizers.push(...findCSharpFullPathStartsWithGuardSanitizers(code), ...findCSharpEarlyReturnGuardSanitizers(code, language));
510
510
  }
511
511
  // -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
512
512
  // fromJson recognized as ETE terminator (does not affect
@@ -5641,6 +5641,113 @@ function guardRejects(lines, i, terminatorRe, window) {
5641
5641
  * form `if (x.StartsWith(root)) { … }` still reports, and a test pins that so
5642
5642
  * the gap stays visible rather than silently assumed.
5643
5643
  */
5644
+ /**
5645
+ * cognium-dev#272 — a validating predicate followed by an EARLY RETURN is a
5646
+ * guard, and the value reaching the sink afterwards is already validated.
5647
+ *
5648
+ * if (Array.IndexOf(Allowed, u.Host) < 0) return; // host allow-list
5649
+ * await client.GetStringAsync(input); // reported ssrf
5650
+ *
5651
+ * if (!input.All(char.IsLetterOrDigit)) return; // char-class check
5652
+ * doc.SelectSingleNode("/a[@b='" + input + "']"); // reported xpath
5653
+ *
5654
+ * Both safe mirrors are clean of their *family* finding after #272's earlier
5655
+ * fixes, and both still emit a finding because the guard itself is not
5656
+ * credited. The shape is shared with #286-B and with the Java `Set.contains`
5657
+ * guard in #293, so the recogniser is written around the PREDICATE rather than
5658
+ * around any one sink family.
5659
+ *
5660
+ * Deliberately limited to predicates that are self-evidently validating:
5661
+ * allow-list membership and character-class validation. A user-defined
5662
+ * boolean helper (`if (!IsAllowed(input)) return;`, #286-B) is NOT credited
5663
+ * here — crediting an arbitrary predicate by name would silence a real flow
5664
+ * whenever the helper does not actually validate, which is a false negative no
5665
+ * test would catch. That half needs its own evidence.
5666
+ *
5667
+ * Only the REJECTING direction counts, via `guardRejects`: a guard whose body
5668
+ * falls through has not stopped anything.
5669
+ */
5670
+ function findCSharpEarlyReturnGuardSanitizers(code, language) {
5671
+ if (language !== 'csharp')
5672
+ return [];
5673
+ const sanitizers = [];
5674
+ const lines = code.split('\n');
5675
+ const terminatorRe = /\b(throw|return)\b/;
5676
+ // Allow-list membership, rejecting form:
5677
+ // if (Array.IndexOf(Allowed, x) < 0) return;
5678
+ // if (!Allowed.Contains(x)) return;
5679
+ const allowlistRes = [
5680
+ /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
5681
+ /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/,
5682
+ ];
5683
+ // Character-class validation, rejecting form:
5684
+ // if (!input.All(char.IsLetterOrDigit)) return;
5685
+ const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
5686
+ for (let i = 0; i < lines.length; i++) {
5687
+ let guardedExpr = null;
5688
+ for (const re of allowlistRes) {
5689
+ const m = re.exec(lines[i]);
5690
+ if (m) {
5691
+ guardedExpr = m[1];
5692
+ break;
5693
+ }
5694
+ }
5695
+ if (!guardedExpr) {
5696
+ const m = charClassRe.exec(lines[i]);
5697
+ if (m)
5698
+ guardedExpr = m[1];
5699
+ }
5700
+ if (!guardedExpr)
5701
+ continue;
5702
+ if (!guardRejects(lines, i, terminatorRe, 6))
5703
+ continue;
5704
+ // The guarded expression may be a derivation of the tainted value
5705
+ // (`u.Host` guards `u`, and `u` came from `input`), so credit the root
5706
+ // identifier as well as the full expression.
5707
+ const root = guardedExpr.split('.')[0];
5708
+ const guarded = new Set([guardedExpr, root]);
5709
+ const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
5710
+ // Walk BACKWARD first: the guard often validates a DERIVATION of the
5711
+ // tainted value while the sink uses the original.
5712
+ //
5713
+ // var u = new Uri(input);
5714
+ // if (Array.IndexOf(Allowed, u.Host) < 0) return; // guards u.Host
5715
+ // await client.GetStringAsync(input); // uses input
5716
+ //
5717
+ // Allow-listing the host of the URI built from `input` constrains where
5718
+ // `input` can point, so the original is guarded too. Bounded to the
5719
+ // identifiers that actually fed the guarded root, transitively.
5720
+ for (let b = i - 1; b >= 0; b--) {
5721
+ const a = assignRe.exec(lines[b]);
5722
+ if (!a || !guarded.has(a[1]))
5723
+ continue;
5724
+ for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? [])
5725
+ guarded.add(id);
5726
+ }
5727
+ // Anything assigned FROM a guarded value downstream is guarded too.
5728
+ const mentions = (t) => {
5729
+ for (const n of guarded)
5730
+ if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(t))
5731
+ return true;
5732
+ return false;
5733
+ };
5734
+ for (let l = i + 1; l < lines.length; l++) {
5735
+ const a = assignRe.exec(lines[l]);
5736
+ if (a && mentions(a[2]))
5737
+ guarded.add(a[1]);
5738
+ if (mentions(lines[l])) {
5739
+ sanitizers.push({
5740
+ type: 'csharp_early_return_guard',
5741
+ method: 'guard',
5742
+ line: l + 1,
5743
+ sanitizes: ['ssrf', 'xpath_injection', 'ldap_injection', 'path_traversal',
5744
+ 'command_injection', 'sql_injection', 'external_taint_escape'],
5745
+ });
5746
+ }
5747
+ }
5748
+ }
5749
+ return sanitizers;
5750
+ }
5644
5751
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
5645
5752
  const sanitizers = [];
5646
5753
  const lines = code.split('\n');