Why your PAC file's URL rules stopped working for HTTPS
Somewhere in a production PAC file near you, there's a rule that
matches part of a URL — an /admin/ path, a keyword, a
query string. It was tested when it was written. It worked. And at
some point in the last several years it silently stopped doing
anything for the vast majority of your traffic, without one error,
one alert, or one changed line in the file.
This is the story of how browsers quietly took the URL away from your PAC file, why they were right to do it, and what to do about the rules that never got the memo.
what your PAC file actually sees now
You'd expect FindProxyForURL(url, host) to receive the
URL the user requested. For http:// traffic, it still
does. For https:// — the overwhelming majority of your
traffic — modern browsers hand your PAC file this instead:
// what the user requested:
https://portal.corp.example/admin/console?session=8f3a...
// what FindProxyForURL(url, host) is actually given:
url = "https://portal.corp.example/"
host = "portal.corp.example"
Path: gone. Query string: gone. Fragments and embedded credentials:
also gone. Per
Chromium's
own proxy documentation, what survives is a
scheme://host:port/ URL — so scheme, hostname, and
port are the whole input surface (port rules still
work; don't rip those out). Any rule that inspects url
beyond that — shExpMatch(url, "*/admin/*"),
url.indexOf("download"), a query-parameter check — is
dead code for HTTPS traffic, and it fails the
worst way possible: by never matching, so the request falls through
to whatever rule sits below it.
why browsers did this
It was a deliberate privacy fix, and honestly, a correct one. A PAC file is remotely-fetched code that gets consulted for every request — and historically it received full URLs, including paths and query strings that carry session tokens, document names, and search queries. Combine that with WPAD auto-discovery (where a client on a hostile network can be handed a malicious PAC file automatically) and you have a URL-exfiltration machine. This wasn't theoretical — it was demonstrated on stage: "Crippling HTTPS with Unholy PAC" (Itzik Kotler & Amit Klein, Black Hat USA 2016), alongside the badWPAD research on WPAD-domain squatting. Browser vendors responded by cutting off what PAC scripts can see. And to kill a common misreading: the stripping applies to every PAC, including the one you explicitly configure — WPAD just made the urgency obvious.
The timeline, in Chrome's case (per MDN and Chromium's docs):
- Chrome 52 (2016): stripping becomes the
default, with an enterprise policy
(
PacHttpsUrlStrippingEnabled) or the--unsafe-pac-urlflag to disable it. - Chrome 74: the policy is removed; only the flag remains.
- Chrome 75 (2019): the flag is gone. There is no way to disable it.
Two scope details that matter: it's not just https://
— everything except http:// gets sanitized, including
wss://, so secure-WebSocket routing rules are dead
code too. And Firefox strips by default as well, though it still
has an about:config escape hatch
(network.proxy.autoconfig_url.include_path) that
Chrome no longer offers. Chromium-based Edge inherits Chrome's
behavior — with one enterprise-shaped exception worth testing
yourself: IE mode resolves proxy through the old
WinINet stack, which reportedly does not sanitize, meaning one Edge
browser can feed your PAC stripped URLs from normal tabs and full
URLs from IE-mode tabs.
Was it the right call? For privacy, yes — though it's also a case
study in breaking a documented interface with no runtime signal,
which is exactly why posts like this one have to exist. Note the
asymmetry that confuses every test plan: http:// URLs
still arrive intact — so a quick test against an http site "proves"
the rule works right before it does nothing in production. Don't
build new logic on http paths either; Chromium has signaled
interest in aligning http eventually.
the symptoms in the wild
- The path-based exception that stopped excepting.
Routing
*/admin/*to an audit proxy, sending a/download/path DIRECT for performance — for HTTPS destinations, those requests now take the same path as everything else on that host, and nobody notices until something depends on the difference. - "It works in the lab." The rule matches when
tested against
http://URLs or in an old PAC debugger that passes full URLs — and silently no-ops in every modern browser. - Fleet inconsistency. A single Windows endpoint can run at least four PAC consumer classes: browsers (stripped), WinHTTP/WinINet services and .NET apps (their own engines), Edge IE-mode tabs (see above), and SSE client connectors that evaluate the PAC themselves — and what those pass is per-product, so verify before assuming. The same PAC file behaves differently per application, which is a miserable thing to discover mid-incident.
check if you're affected
Grep your PAC file for its first parameter — every place
url is used in a condition, ask whether the match
needs anything past scheme, host, or port. Or paste the file into
the PAC File Tester: the linter
flags URL-path rules as dead code for HTTPS, and the trace runs
your test URL through both behaviors — modern (stripped) and
legacy (full URL) — and warns you when the routing decision
differs.
To see it live on a real browser: capture a
chrome://net-export log and read the proxy-resolution
events — they show the exact sanitized URL your PAC received (and
any alert() output from the PAC lands in the same
log). Firefox-side, run with MOZ_LOG=proxy:5. (Safari
evaluates PAC through CFNetwork — I haven't verified its behavior;
test before assuming either way.)
what to do instead
Match on host (and port), not path. Hostname and
port still arrive intact — they're the stable identity a PAC
decision can hang on. If two application areas need different
routing, the real fix is giving them different hostnames
(portal.corp.example vs
admin.corp.example). Be honest about the cost — that's
a DNS entry, a certificate, and an app-team conversation, not a PAC
edit — but it's a fix that works, versus a rule that can't.
(Path-splitting one HTTPS host across proxies was fragile even
before stripping — HTTP/2 connection reuse never respected your
rule boundaries.)
Do path-level policy at the proxy, not in the PAC. This one matters for SSE shops: your secure web gateway sits after TLS inspection and sees the full decrypted URL — path, query, everything. URL-filtering policy, path-based blocks, tenant restrictions — that logic belongs in the platform's policy engine, not in the PAC. One honest caveat: this only holds where inspection is actually on. For bypassed or cert-pinned categories, nobody sees the path — the real answer there is that path-level policy doesn't exist for those flows.
Scheme checks still work. The scheme survives
sanitization, so url.substring(0, 6) === "https:" or
a wss: scheme test is fine. (Retire the classic
ftp:* example, though — browsers removed FTP support
entirely back in 2021, so that rule is archaeology.)
Document the per-stack difference. If you must keep a URL-path rule for a legacy non-browser consumer, comment it loudly: this rule only fires for clients that pass full URLs — browsers will skip it. The next engineer will assume it works everywhere; the comment is cheaper than the incident.
the bigger lesson
The PAC file's input surface shrank and almost nobody re-read their rulebase afterward. That's the general shape of a whole class of outages: the platform underneath a config changes its contract, the config keeps parsing cleanly, and the behavior drifts without a single error. Files that "never changed" still rot.
Treat the PAC as what it is now — a coarse, host-level traffic director — and audit it like the firewall policy it secretly is. Top-down, first match wins, and every rule should answer one question: can this condition still see what it's matching on?
check your own PAC in four seconds
The PAC File Tester runs your file against both browser behaviors and flags dead URL-path rules, shadowed rules, DNS traps, and risky fallbacks — entirely in your browser; your PAC never leaves your machine. Subscribe and the one-page PAC cheat sheet is yours as soon as you confirm:
ack — check your inbox for a confirmation link.
One confirmation email, then you're in. Unsubscribe anytime.