/** * 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)); }