File size: 2,683 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
 * Passthrough / bypass logic for the MITM server.
 * Determines which hostnames should be tunneled without TLS decryption.
 *
 * Precedence: bypass list > target match > passthrough default.
 * Source: plano 11 §4.6 (origin: llm-interceptor filters.py::ignore_hosts)
 */

/**
 * Built-in bypass patterns — hosts that must NEVER be TLS-decrypted.
 * Banks, government sites, and corporate SSO providers.
 */
export const DEFAULT_BYPASS_PATTERNS: RegExp[] = [
  /\.bank\./i,
  /(^|\.)gov(\.|$)/i,
  /(^|\.)okta\.com$/i,
  /(^|\.)auth0\.com$/i,
];

/**
 * Match a hostname against a simple glob pattern (only * as wildcard, no ** or ?).
 * Implemented without RegExp to avoid ReDoS on user-supplied patterns (CWE-1333).
 * Uses a linear split-and-check algorithm: split by '*', verify each segment appears
 * in order within the lowercase hostname.
 */
export function globMatch(hostname: string, pattern: string): boolean {
  // Guard: reject patterns with more than 8 segments (after split) to bound complexity
  const segments = pattern.toLowerCase().split("*");
  if (segments.length > 9) return false;

  const h = hostname.toLowerCase();

  // No wildcard — exact match
  if (segments.length === 1) return h === segments[0];

  // Must start with the first segment (if non-empty)
  const first = segments[0];
  if (first && !h.startsWith(first)) return false;

  // Must end with the last segment (if non-empty)
  const last = segments[segments.length - 1];
  if (last && !h.endsWith(last)) return false;

  // Walk through middle segments verifying each appears after the previous match
  let pos = first.length;
  for (let i = 1; i < segments.length - 1; i++) {
    const seg = segments[i];
    if (seg === "") continue; // consecutive wildcards — skip
    const idx = h.indexOf(seg, pos);
    if (idx === -1) return false;
    pos = idx + seg.length;
  }

  // Ensure the last fixed segment doesn't overlap with middle matches
  if (last) {
    const minEnd = pos + last.length;
    if (minEnd > h.length) return false;
  }

  return true;
}

/**
 * Determine if a hostname should be bypassed (tunneled without TLS decryption).
 *
 * @param hostname - The target hostname (SNI or Host header value)
 * @param userBypass - User-configured bypass patterns (glob strings or regexes)
 * @returns true if the hostname should be tunneled without inspection
 */
export function shouldBypass(hostname: string, userBypass: string[]): boolean {
  // Default bypass patterns take precedence
  if (DEFAULT_BYPASS_PATTERNS.some((re) => re.test(hostname))) return true;

  // User-defined bypass patterns (glob strings)
  return userBypass.some((p) => globMatch(hostname, p));
}