The Polyfill.io supply-chain attack: how a CDN dependency went rogue
For years the advice was boring and correct: if you need a JavaScript polyfill, point a <script> tag at cdn.polyfill.io and the service will hand each browser exactly the shims it lacks, nothing more. The service read the User-Agent, computed the feature gap, and returned a tailored bundle. That tailoring was the selling point. It was also the reason the same defense everyone recommends for third-party scripts could not be applied to this one. You cannot pin a hash to a file whose bytes change with every browser.
In February 2024 the domain and its GitHub account were sold. Four months later, sites that had embedded that script tag and forgotten about it were quietly redirecting some of their mobile visitors to a sports-betting page through a fake analytics domain spelled to look like Google’s. Nothing in the HTML of those sites had changed. The compromise lived entirely at the other end of a src attribute that a developer had typed once, years earlier, and never thought about again. This post traces exactly how that happened.
The sections below walk through the service and why it existed, the ownership transfer and the warnings that preceded the attack, the structure of the injected payload and the conditions it checked before firing, the scale of the blast radius as three different scanners measured it, the takedown by registrars and CDNs, and the SRI question that hangs over the whole story. It closes with the 2025 sanctions that named the buyer.
What Polyfill.io was and why people trusted it
A polyfill is a chunk of JavaScript that implements a browser feature the running browser lacks, so application code can call a modern API without checking whether it exists. Array.prototype.includes, Promise, fetch, IntersectionObserver: each has a polyfill that does nothing on a browser that already ships the native version and supplies a working fallback on one that does not. The polyfill-service project, originally built at the Financial Times and later maintained as open source, turned this into a hosted product. You requested https://cdn.polyfill.io/v3/polyfill.min.js, the server parsed your User-Agent, decided which features that browser was missing, and assembled a bundle containing only those shims.
That per-browser tailoring is the detail that matters for everything downstream. The response to polyfill.min.js was not a static file. A request from an ancient Android WebView got one set of shims; a request from current Chrome got a near-empty stub because modern Chrome needs almost nothing. The service computed the diff at request time against an internal feature database. From the site owner’s side this was the whole appeal. You shipped one URL and every visitor, on whatever browser, received precisely the patch they needed and no dead weight.
The creator of the project, Andrew Betts, has been clear about a boundary that became important later. He built the service but never owned the domain name and had no say in its sale. Back in February 2024, before any malicious behavior had been observed, Betts publicly advised sites to stop using polyfill.io altogether, pointing out that modern browsers no longer need most of what the service shipped. That advice landed as a footnote at the time. It read very differently in June.
The domain sale and the warnings nobody acted on
In February 2024 a company operating under the name Funnull bought the polyfill.io domain and the associated GitHub account. The polyfill-service code is open source, so the buyer did not acquire a secret. They acquired something more useful: the live endpoint that hundreds of thousands of pages already trusted, and the publishing rights to the canonical repository.
Cloudflare watched this happen and did not like it. In the same month, it flagged the ownership transfer as a supply-chain risk, describing the new owner as a relatively unknown company with no track record of trust to administer a service of that reach. Cloudflare’s stated estimate at the time was that roughly four percent of all websites used polyfill.io in some form. To hedge, Cloudflare stood up its own mirror of the service on cdnjs in February, so that anyone who wanted the polyfills without the risk had a clean source. That mirror sat mostly unused for four months while the original kept serving traffic.
The warning was public, specific, and early. It was also, for the overwhelming majority of affected sites, ignored. The script tag had been pasted in long ago. No one was watching the domain’s WHOIS records. This is the recurring shape of CDN supply-chain risk: the dependency is invisible after install, so a change of control at the origin is invisible too, right up until the origin starts behaving badly.
*The four-month gap between the February ownership transfer and the late-June detection is the whole problem: a trusted endpoint kept serving traffic while control had already changed hands.*Anatomy of the injection
The Sansec forensics team published its analysis on 25 June 2024, and that report is the closest thing to a primary technical record of what the malicious script actually did. The behavior it described was not a crude redirect bolted onto the polyfill bundle. It was a conditional, multi-stage loader that went to some length to fire only for the right victims and to stay quiet for everyone who might notice.
The injection only acted on a narrow slice of requests. It targeted mobile devices, matched against specific user agents, and skipped desktop entirely. Sansec described an isPc() check that restricted execution to non-Windows and non-Mac systems, so the payload ran on phones and not on the laptops of the developers most likely to be looking at network traces. When the conditions were met, the script pulled a secondary stage and then redirected the visitor. The redirect endpoint observed by Sansec was a sports-betting page, reached through a domain crafted to read like a Google property at a glance: www.googie-anaiytics.com, with the letter substitutions that typosquatting relies on. A loader script named checkcachehw.js sat in front of the redirect, and the code carried what Sansec called specific protection against reverse engineering.
The conditional firing is worth dwelling on, because it explains why the attack ran for months without obvious complaints. A payload that redirected every visitor on every load would have generated support tickets within hours. This one did not.
The staging mattered as much as the gates. The script a browser pulled from polyfill.min.js did not carry the redirect inline. It carried the logic to decide whether to fetch a second stage, and only then loaded checkcachehw.js and whatever the next hop served. Splitting the payload this way buys the attacker two things. The first-stage bundle stays small and looks plausible next to the legitimate polyfills it was bundled with, so a casual diff of the served JavaScript does not scream “malware.” And the second stage is fetched fresh, which means the operator could change the destination, tune the conditions, or pull the payload entirely without touching the script every site had cached a reference to. A researcher capturing the bundle once might never see the redirect at all if their capture missed the conditions or the second stage had been swapped out. That decoupling, benign-looking loader in front, swappable hostile stage behind, is a standard malware pattern and it is also why early reports varied on exactly what the script did. Different observers, at different times, on different devices, saw different things.
The evasion logic
Three checks did most of the hiding. The first looked for an administrator. Sansec reported that the code inspected document.cookie for the presence of admin_id and adminlevels, and stayed dormant when it found them. The reasoning is plain enough: the person logged in as admin is the person who would investigate a report, so do not show them anything. This is the same instinct behind cloaking in the SEO-spam world, applied to a script.
The second check looked for analytics. When the page carried a recognized web-analytics service, the payload delayed its execution. Sansec named the services it watched for, a list that included Baidu Tongji, CNZZ, Matomo, Google Analytics, Google Tag Manager, Plausible, and StatCounter. The point was to avoid generating a visible spike of outbound navigations or redirect events in whatever dashboard the site owner checked. A redirect that does not show up in analytics is a redirect nobody files a ticket about.
The third check was time. Rather than fire on every qualifying load, the payload rolled against the hour of day. Sansec’s analysis described low redirect probabilities in the small hours and higher ones later: on the order of a ten percent chance between midnight and 2am, fifteen percent from 2 to 4am, twenty percent from 4 to 7am. The exact thresholds across the full day are not fully enumerated in public write-ups, and the figures above are the ones Sansec documented; treat anything beyond them as not precisely known. The structure of the idea is the takeaway. Probabilistic, time-gated firing turns a deterministic redirect into something that looks like flaky, intermittent weirdness, which is exactly the kind of bug report that gets closed as “could not reproduce.”
Put the three together and you have a payload tuned for longevity. It avoids the people who can stop it, avoids the instruments that would record it, and fires rarely enough that the noise blends into the background. The architecture of detection evasion on the modern web, whether in anti-bot scripts or in malware like this, leans on exactly these signals; our write-up on anti-bot honeypots covers the defensive side of the same coin, and the server-side vs client-side bot detection split explains why client-resident logic like this is so hard to observe from the origin.
Why this script and not the others on the page
A typical page loads scripts from many origins. What made the polyfill case acute is that the compromised script ran with the full privileges of the first-party page. A <script src> with no sandboxing executes in the same JavaScript context as the site’s own code. It can read document.cookie for any cookie not marked HttpOnly, rewrite the DOM, issue same-origin fetches, and call location.assign to navigate the user away. The browser draws no line between code the site author wrote and code a CDN served under the author’s script tag. This is the trust model that Subresource Integrity and a tight Content Security Policy exist to constrain, and it is the model the attack exploited.
The cookie read deserves a note. The payload’s admin check worked by reading document.cookie, which means it could only see cookies that were not protected with the HttpOnly attribute. Session cookies set with HttpOnly are invisible to JavaScript by design, which is the entire reason that attribute exists. A site that marked its admin session cookie HttpOnly would have denied the script the very signal it used to detect admins, although in this case the attacker wanted to detect admins to avoid them rather than to impersonate them. The mechanics of that attribute and the rest of the cookie hardening surface are covered in cookie security: HttpOnly, Secure, SameSite, and the __Host- prefix.
Measuring the blast radius
Estimates of how many sites were affected ranged widely, and the spread itself is instructive. Sansec’s headline figure was more than 100,000 sites. Cloudflare, looking at its own traffic and its four-percent-of-the-web estimate, framed it as several hundreds of thousands. The most concrete count came from Censys, which on 2 July 2024 reported 384,773 hosts embedding a polyfill script that linked to the malicious domain.
Those numbers measure slightly different things, which is why they disagree. “Sites” and “hosts” are not the same unit, detection windows differ, and a single high-traffic site can dwarf thousands of dead pages in actual user exposure. Censys also found the contamination ran wider than one domain. Through a Cloudflare API key that had leaked, investigators tied the same operator to four further CDN-style domains: bootcdn.net, bootcss.com, staticfile.net, and staticfile.org. Censys counted on the order of 1.6 million hosts referencing one or more of those four. Some of that activity predated the polyfill takeover; bootcss had shown suspicious behavior going back to mid-2023.
The disagreement between scanners is not a footnote; it is the reason “how big was it” has no single answer. Sansec was reporting compromised behavior it had analyzed. Cloudflare was reasoning from its own traffic share and a population estimate. Censys was counting hosts that still carried a script tag pointing at the bad domain at one instant on 2 July, after a week of remediation had already pulled some sites off it. Censys separately noted that on the order of 216,000 hosts had already migrated to a Fastly or Cloudflare mirror by the time it scanned, which means the live-exposure number on any given day was a moving target sliding downward fast. None of these figures is wrong. They answer different questions, and a reader who wants a defensible number has to say which question they mean.
Among the affected hosts were names that made the story land harder. Public reporting and the Censys analysis pointed to references on properties tied to large organizations, including major media and corporate sites, and Censys flagged on the order of 182 hosts on .gov domains. A large share of affected hosts clustered in one network: Censys placed roughly 237,700 of them inside Hetzner’s AS24940, mostly in Germany, which says more about where cheap hosting lives than about who was targeted.
The takedown
The response came from several directions at once, because no single party could fix it for everyone. The registrar acted first on the domain itself. Namecheap suspended polyfill.io on 27 June 2024, two days after the Sansec report went public, which killed the live endpoint and stopped fresh malicious responses from that name. A handful of stragglers persisted on alternate hostnames for a while; Censys still saw a small number of hosts presenting wildcard.polyfill.io.bsclink.cn as of 2 July, served out of a Singapore-based ASN.
The CDNs took a different route, aimed at the embedded src attributes that registrar action alone could not change. Cloudflare shipped automatic rewriting: for sites it proxied, it parsed HTML responses, found <script> tags pointing at polyfill.io, and rewrote the src to its own clean mirror on cdnjs, https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js. The rewrite was on by default for free-plan customers and a single toggle for paid plans, under Security settings. It skipped pages that already carried a Content Security Policy header, to avoid rewriting a script into a form the page’s own CSP would then block. Fastly, which had historically hosted polyfill infrastructure, likewise offered a safe mirror. Browser vendors moved too: blocklists in Google’s Safe Browsing ecosystem and in some ad blockers began flagging the domain.
The fastest individual fix, and the one the project’s own creator recommended, was the bluntest. Delete the script tag. Betts had already argued in February that most sites no longer needed any of the polyfills the service shipped, because the browsers that needed them had aged out of the supported population. For a great many sites the correct remediation was not to swap in a mirror but to remove a dependency that had stopped earning its place years earlier.
*Three layers of response, each fixing a different part of the dependency. The registrar killed the source, the CDNs patched the link, and the simplest fix was to drop a dependency that had outlived its purpose.*The SRI problem at the heart of it
Here is the uncomfortable part. The standard defense for a third-party script is Subresource Integrity. You add an integrity attribute carrying a cryptographic hash of the exact bytes you expect, and the browser refuses to execute the script if what it downloads does not hash to that value. SRI is precisely the control that would have neutered this attack: a tampered bundle would not match the pinned hash, and the browser would have blocked it. Our Subresource Integrity deep dive covers the mechanism in full.
SRI could not be used here, and the reason is structural rather than negligent. SRI hashes bytes. Polyfill.io served different bytes to different browsers on purpose. As Terence Eden laid out back in 2018, years before the attack, a site that wanted to pin the polyfill response would need not one hash per URL but a hash per (URL, User-Agent) pair, and would have to vary the integrity attribute by browser. SRI does support listing multiple acceptable hashes in one attribute, so in theory you could enumerate the hashes for every browser you cared about. In practice that list would be enormous, would need updating whenever the service added or changed a single polyfill, and would silently break for any browser whose tailored bundle you had not pre-hashed. The dynamic behavior that made the service convenient is the same property that made the standard integrity control inapplicable.
That is the lesson worth carrying out of this incident. SRI is excellent against static third-party assets and useless against dynamic ones, and a CDN that varies its response by client request sits squarely in the blind spot. The defensive answer is not a better hash. It is to recognize that a dynamic third-party script is an open-ended grant of execution to whoever controls that origin, today and forever, and to price that grant accordingly. Either you accept that you are trusting the origin’s owner indefinitely, or you pin a specific static version and host it yourself, or you delete the dependency. Content Security Policy can narrow the blast radius by constraining what an executing script may connect to or where it may navigate, which is why CSP and SRI are usually deployed together; the CSP post explains why even that is hard to get right.
It is also a near-perfect echo of the other supply-chain incidents of the same era. The mechanism differs, but the shape is identical: a dependency that thousands of projects trusted by reference rather than by content, and an attacker who acquired control of the reference. The npm event-stream compromise turned on a handed-off maintainer account. The XZ Utils backdoor turned on a patiently cultivated maintainer relationship. Dependency confusion turned on a naming gap. Polyfill turned on a domain sale. In every case the trusted thing changed hands while the trust did not move with it.
Attribution and the 2025 sanctions
The story did not end with the takedown. On 29 May 2025 the U.S. Treasury’s Office of Foreign Assets Control sanctioned Funnull Technology Inc., a Philippines-based firm, and its administrator Liu Lizhi. The Treasury action tied Funnull to large-scale cryptocurrency investment fraud, the pattern often called pig butchering, and stated that Funnull’s operations had defrauded U.S. victims of more than 200 million dollars. OFAC designated two cryptocurrency addresses, one on Ethereum (0xd5ED34b52AC4ab84d8FA8A231a3218bbF01Ed510) and one on Tron (TNmRfnSUXZoWWzxcDDbf95eGQYXt1mJDt8).
The mechanism Treasury described is what makes the polyfill episode read as one tactic inside a larger operation rather than a one-off. According to the action, Funnull’s business was buying IP address space in bulk from legitimate cloud providers and reselling it to scam operators, a practice analysts have called infrastructure laundering: the malicious sites inherit the clean reputation of a major cloud’s address ranges. Reporting on the sanctions, citing the underlying findings, connected Funnull to a large volume of scam domains and named the 2024 polyfill compromise explicitly as an example of how the operator co-opted legitimate web infrastructure, redirecting visitors of trusted sites to scam and gambling pages. The exact internal counts in the various reports differ by source and date, so where a specific figure matters, the original Treasury and Censys documents are the ones to cite rather than the secondary summaries.
What the sanctions confirmed is that the polyfill attack was not the work of an opportunist who got lucky with a cheap domain. It was a node in a standing apparatus for laundering reputation, one that treated a beloved open-source CDN as a distribution channel for fraud. The buy was rational. A domain trusted by hundreds of thousands of pages is a better delivery vehicle than any amount of fresh, unknown infrastructure, precisely because nobody re-checks a script tag they pasted in years ago.
What the incident actually taught
The durable lesson of Polyfill.io is not “audit your dependencies,” which everyone already says and few do. It is narrower and more useful. A <script src> pointing at a third-party origin is a permanent, unbounded delegation of code execution to whoever controls that origin, and control can change without any signal reaching you. The bytes you tested against are not the bytes the next visitor receives. For a static asset you can close that gap with an integrity hash. For a dynamic one you cannot, and a dynamic one is exactly what a per-browser polyfill service is.
The attack worked because the defensive control that would have stopped it was structurally inapplicable to the thing it needed to protect, and because the dependency had become invisible. It was caught not by the sites running it but by an external forensics team watching CDN behavior from the outside, four months after control had quietly changed hands. The cleanest fix for most victims turned out to be deletion, because the dependency had stopped doing anything useful long before it started doing something hostile. A dead dependency you forgot to remove is not inert. It is a live grant of trust, waiting for someone to buy the other end.
Sources & further reading
- Sansec (2024), Polyfill supply chain attack hits 100K+ sites — the primary forensic report describing the payload, the
googie-anaiytics.comredirect, and the admin, analytics, and time-based evasion checks. - Cloudflare (2024), Automatically replacing polyfill.io links with Cloudflare’s mirror for a safer Internet — the CDN’s rewrite mechanism, the cdnjs mirror URL, the February warning, and the four-percent-of-the-web estimate.
- Censys (2024), Polyfill.io Supply Chain Attack: Digging into the Web of Compromised Domains — the 384,773-host count, the four sister CDN domains, the Hetzner AS24940 concentration, and the lingering
bsclink.cnhosts. - U.S. Department of the Treasury (2025), Treasury Takes Action Against Major Cyber Scam Facilitator — the OFAC sanctions designating Funnull Technology Inc. and Liu Lizhi, with the pig-butchering context and the polyfill reference.
- Chainalysis (2025), OFAC Sanctions Funnull Technology for Pig Butchering Scams — the sanction date, the $200M figure, and the designated Ethereum and Tron addresses.
- Terence Eden (2018), Dynamic JavaScript and SRI — the pre-attack argument for why per-User-Agent responses defeat Subresource Integrity, requiring a hash per (URL, UA) pair.
- The Hacker News (2024), Polyfill.io Attack Impacts Over 380,000 Hosts — secondary reporting corroborating the host counts and the spread to affected organizations.
- Sonatype (2024), Polyfill.io Supply Chain Attack Hits 100,000 Websites: All You Need to Know — overview of the February sale to Funnull and the redirect behavior.
- The Record / Recorded Future News (2024), Polyfill, Cloudflare trade barbs after reports of supply chain attack — Andrew Betts’ “remove it immediately” advice and his statement that he never owned the domain.
- Krebs on Security (2025), Big Tech’s Mixed Response to U.S. Treasury Sanctions — follow-up on how cloud providers responded to the Funnull designation and the infrastructure-laundering model.
Further reading
The anatomy of a CDN cache key and why cache poisoning happens
Traces what a CDN actually puts in its cache key, how unkeyed headers and parser discrepancies turn a shared cache into an exploit delivery system, and the defenses that hold up against poisoning and deception.
·20 min readWeb cache deception: how path confusion tricks a CDN into caching secrets
Traces web cache deception from Omer Gil's 2017 PayPal disclosure through the 2020 and 2022 measurement studies to the 2024 delimiter research, and the defenses that actually close the cache-versus-origin gap.
·21 min readSubresource Integrity and the supply-chain risk of third-party scripts
Traces how the integrity attribute verifies a third-party script against a cryptographic hash, what a compromised CDN it stops, the dynamic-resource gap it cannot close, and why adoption stayed in single digits.
·20 min read