The 20 most dangerous PAC mistakes

Ranked by how often they appear in real PAC files and how much damage they do. Most of them do not break anything visibly; they quietly route traffic around the proxy.

  1. #1 A broken PAC plus a permissive outbound firewall

    Every PAC failure (no default return, a parse error, a malformed return value) falls back to a direct connection. If the firewall lets clients out on 80/443, that failure is silent and uncontrolled internet access. Default-deny client egress so PAC failures become loud. See: no-default-return, syntax-error, invalid-return-value, proxy-without-port.

  2. #2 Hostname pattern applied to url instead of host high

    url is the complete request URL (scheme, host, port, path, query); host is the bare hostname. A hostname pattern tested against url either never matches or matches far too much. The most common PAC mistake.

  3. #3 Trailing * in a host pattern matches attacker-controlled suffixes high

    shExpMatch(host, "example.com*") matches example.com.evil.test and example.comms.test. Anyone who controls a domain can append the trusted name as a subdomain label and inherit the rule.

  4. #4 dnsDomainIs pattern without a leading dot matches look-alike domains high

    dnsDomainIs is a plain string-suffix test. "corp.example" matches www.corp.example, corp.example and also notcorp.example. Only ".corp.example" restricts the match to subdomains.

  5. #5 Leading * without a dot matches look-alike domains high

    shExpMatch(host, "*corp.example") matches badcorp.example as well as www.corp.example. The safe form is "*.corp.example", where the dot forces a label boundary.

  6. #6 PAC served over plain HTTP high

    The PAC is executable routing policy fetched on every browser start. Over plain HTTP a network attacker (hostile Wi-Fi, compromised router, ISP middlebox) can rewrite it and route all web traffic through a proxy of their choice.

  7. #7 No unconditional return at the end of FindProxyForURL critical

    Some code path reaches the end of FindProxyForURL without a return. The function then returns undefined, and Chromium, Firefox and WinHTTP all connect directly. Traffic silently bypasses the proxy.

  8. #8 Pattern matches an entire top-level domain high

    shExpMatch(host, "*.de") or dnsDomainIs(host, ".com") routes a whole country or generic TLD. In the usual "proxy everything, except..." PAC this bypasses inspection for millions of unrelated sites.

  9. #9 IP range matched as a text prefix high

    shExpMatch(host, "127.*") or "10.*" compares characters, not addresses. It matches the hostname 127.foo.evil.test and, for "172.*", every public 172.x address. Use isInNet with a mask.

  10. #10 PAC distributed via WPAD DNS discovery medium

    WPAD clients look for wpad.<suffix>, strip one label and retry up to wpad.<tld>. If no internal record answers, a registered public wpad name can supply the PAC. Own the name, block the walk at the resolver, prefer explicit configuration.

  11. #11 PAC URL distributed via DHCP option 252 medium

    DHCP answers are plaintext broadcasts with no authentication in practice. A rogue DHCP responder on the same segment can hand out its own PAC URL and take over routing for every client that trusts option 252. Switch-level DHCP snooping or explicit configuration closes the gap.

  12. #11 DHCP option 252 is unauthenticated

    Any device on the segment that answers DHCP can hand out its own PAC URL. Distribute the PAC URL explicitly (GPO/MDM) where you can. The rule for this is still in review. See: wpad-dns-discovery.

  13. #12 String prefix used to match an IP range high

    host.substring(0, 8) == "10.1.2.9" matches 10.1.2.99 and the hostname 10.1.2.9.evil.test. Prefix arithmetic on strings is not subnet arithmetic.

  14. #13 DNS lookup on every request high

    dnsResolve, isResolvable and isInNet on a hostname each cost a DNS round trip inside proxy resolution, for every connection, before the page starts loading. They also make routing depend on the resolver being reachable and honest. Most PACs can decide on the hostname string alone.

  15. #14 Port matched as a substring of the URL high

    shExpMatch(url, "*:1080*") was meant to match port 1080 but also matches http://www.example.com/page?id=1080. A bypass is one query parameter away.

  16. #15 PROXY entry without a port medium

    "PROXY proxy.corp.example" names no port. Chromium assumes 80; other engines are reported to reject the entry, and a rejected sole entry means a direct connection. Always write host:port.

  17. #16 isInNet called with a hostname medium

    isInNet resolves its first argument when it is not already an IP address. Passing host means a DNS lookup for every non-IP request, with all the latency and fail-open behaviour of dnsResolve, and only one of possibly several addresses is checked.

  18. #17 RFC 1918 range with the wrong mask medium

    172.16.0.0 with mask 255.255.0.0 covers only 172.16.x.x. The private block is 172.16.0.0/12 (mask 255.240.0.0), i.e. 172.16.0.0 to 172.31.255.255; the /16 misses fifteen sixteenths of it.

  19. #18 Only 127.0.0.1 excluded instead of 127.0.0.0/8 low

    The entire 127.0.0.0/8 block is loopback. A PAC that only exempts 127.0.0.1 sends 127.0.0.2 or 127.1.2.3 (used by local development tools and some agents) to the proxy, where the connection fails.

  20. #19 Routing decided by myIpAddress() medium

    Each engine chooses "the client's IP" by its own heuristic. With VPNs, Wi-Fi plus Ethernet, virtualisation adapters or IPv6, the answer differs between browsers and changes without a PAC re-fetch. Location-based routing keyed on it is unreliable.

  21. #20 Upper-case letters in a hostname pattern medium

    Browsers hand the PAC a lower-cased hostname. A pattern such as "Intranet.Corp.Example" never matches in Chromium or Firefox. Where engines do not lower-case (WinHTTP), the comparison is case-sensitive anyway, so patterns must be lower case and host should be lower-cased explicitly.