* * deny — ~/blog/pac-file-mistakes

PAC file mistakes that break user traffic (and how to spot them)

the pac series — part 1 of 4 1 · mistakes that break user traffic · 2 · writing your first PAC file · 3 · https path stripping · 4 · watching it decide · the tester

A PAC file that parses cleanly can still route half your traffic somewhere you never intended. That's what makes PAC problems nasty: the file is syntactically correct, the proxy settings deploy fine, nothing throws an error — and users quietly get unexpected behavior that surfaces as a vague ticket three days later.

These are the mistakes I've actually seen cause production pain, written up the way I'd explain them on a bridge call. If you just want the debugging order, skip ahead.

how a PAC file thinks

A PAC file answers one question, once per request: where does this traffic go? The usual answers are one of four buckets — straight out to the internet (DIRECT), to an enterprise proxy or specific data center, to a local proxy agent running on the workstation, or to a cloud security edge like an SSE platform's nearest node.

And it evaluates exactly like a firewall rulebase: top to bottom, first match wins. Once a request matches a rule, nothing below that rule is ever consulted. Every mistake in this post is some version of forgetting one of those two facts.

mistake 1: traffic lands in the wrong bucket

The most common failure isn't broken syntax — it's a correctly written rule that sends traffic to the wrong destination. Usually it comes down to scoping: matching on an FQDN when you meant a wildcard, matching a wildcard far broader than you realized, or mixing hostname-based and IP-based conditions without thinking about which one a given request will actually hit.

The symptom is subtle because everything still works — just wrong. Internal apps egressing straight to the internet. SaaS traffic hairpinning through a data center for no reason. A site that should ride the cloud proxy going direct and skipping inspection. Nobody reports "traffic is taking the wrong path"; they report slow, weird, or intermittently broken.

When you write a rule, be explicit with yourself about two things: what exactly should match it (FQDN? subnet? wildcard — how wide?), and which of the four buckets it must land in. Then verify a real flow actually hits the return statement you think it does.

mistake 2: forgetting it's top-down, first-match-wins

Everyone who's touched a firewall knows evaluation order is top-down. And yet — PAC files trip people constantly, because a PAC file doesn't look like a rulebase. It looks like code. It's still a rulebase.

The classic version: a broad match sitting high in the file quietly swallows requests that were supposed to hit a specific rule further down. The bottom rule isn't broken — it's never evaluated. If you've ever stared at a rule muttering "this should be matching," odds are something above it already ate the request.

// intent: sinkhole one problem domain
if (shExpMatch(host, "*example*")) return "PROXY sinkhole.corp:9999";

// ...twenty lines later, never reached:
if (shExpMatch(host, "app.example-partner.com"))
    return "PROXY cloud-edge.example:80";

This one causes real outages. Get a little too broad near the top of the file — say, while trying to sinkhole some traffic — and you can match far more than you anticipated, in production, everywhere the PAC is deployed at once. Order of operations: specific rules high, broad rules low, and any time you add a wide match, ask what it swallows from every rule beneath it.

mistake 3: the copy-pasted catch-all

This site is named after the firewall version of this principle, so you can guess how I feel about it.

Almost nobody writes a PAC file from scratch. You copy one that works — often a test file — and edit. Test PAC files very reasonably end in return "DIRECT": it's a lab, you want traffic to just work. Then the file gets promoted to production with that fallback still in place, and every request that doesn't match an explicit rule now falls out of your intended forwarding path.

How bad that is depends on the rest of your stack. If the PAC file is the only thing steering traffic — a classic explicit-proxy environment — that's a full bypass: no inspection, no policy, no logs. If you also run a tunnel-based client that grabs traffic at the network layer, the traffic will usually still be captured, inspected, and logged — but it's now riding a path you didn't design, and "works, just not the way the architecture doc says" is its own class of incident.

// bottom of the file. syntax: valid. behavior: not what you designed.
return "DIRECT";

// what production almost always wants instead:
return "PROXY proxy.corp.example:9400";

The cruel part: the file is correct. It validates, it deploys, users browse happily. You find out weeks later when someone asks why a chunk of traffic never appears in the logs, or why forwarding rules aren't working the way the design doc says. The final rule of a PAC file decides the fate of everything nothing else matched — make it a deliberate decision, not an inherited one.

mistake 4: dnsResolve() and friends

Full disclosure: this is the one mistake on this list I've never had to debug in production — because the guidance is clear enough that I don't let these functions into my PAC files in the first place. Zscaler's best-practices doc says to carefully consider any use of isResolvable(), dnsResolve(), and isInNet() due to DNS performance impact, and Microsoft's PAC guidance is blunter: DNS lookups are the most frequent cause of PAC-related hangs and slow browsing, with isInNet() called out by name.

The mechanics: your PAC file runs on every request, and each of those functions fires a live DNS query mid-evaluation while the browser blocks and waits. When DNS is fast, nobody notices. When DNS is slow or unreachable — captive portal, machine still waking up, login happening before the tunnel is fully established — every single request stalls on a lookup that can't complete. That's the classic "browsing hangs for 30 seconds, then everything is fine" pattern, and it gets blamed on the browser, the proxy, and the network before anyone opens the PAC file.

The alternative is plain string matching, which costs nothing:

// fires a live DNS query on every request that reaches it:
if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0")) return "DIRECT";

// string comparison — same intent, no DNS, no stall:
if (shExpMatch(host, "*.corp.example")) return "DIRECT";

If you genuinely need IP-based decisions in a PAC file, keep them as low in the file as possible — below the high-probability string matches — so the vast majority of requests never reach them. And know that you're paying for every request that does.

how I debug a suspect PAC file

Before reading a single rule, in this order:

  1. Confirm which PAC file they're actually running. Real environments have several. Which file should this user get, and is that the one they actually have — in the right version? A shocking number of "PAC bugs" are users on the wrong file or a stale copy, and no amount of rule-reading will find that.
  2. Get the destination — both names and numbers. What FQDN are they hitting, and what IP does it resolve to? Then walk the file top-down for the first match, watching for the sneaky failure: an IP or subnet rule higher in the file that the FQDN resolves into, which you never anticipated when you wrote the hostname rule below it.
  3. Establish the blast radius. On a big shared PAC file: are other users hitting the same resource through the same file just fine? If yes, stop staring at the PAC — your problem is almost certainly somewhere else.

the one hygiene rule: keep a golden copy

If I could make every admin follow a single PAC practice, it's this: keep a known-good golden copy outside the vendor portal. Not every security vendor versions your PAC edits — with some, the moment you save over a working file, the working file is gone. When a bad edit takes down routing for everyone, "roll back to golden" is a two-minute fix. "Reconstruct what it used to say" is a very long night.

the flip side — PAC as a power tool Everything above is about PAC files going wrong, but modern SSE platforms expose surprisingly useful logic inside them. One example: branch on the client's country and the destination. A user in the US who needs a government site in India can be routed out through the platform's in-country data center — geo-restrictions solved in two lines of routing logic, no VPN gymnastics. That recipe and the rest of the constructive side live in the practical PAC file cookbook.