The script tag you added years ago can still run code you didn't write

In June 2024, more than 100,000 websites suddenly started redirecting their visitors to scam pages. The owners hadn't changed anything. The malicious code came from a <script> tag they'd added years earlier, pointing at polyfill.io, a service that quietly filled in browser features for older clients. The domain had changed hands, the new owner pushed malware through it, and every site still loading that script served the attack to its own users. CVE-2024-38526. Nobody got hacked in the usual sense. They just kept trusting a host that stopped being trustworthy.

That's the part worth sitting with. A <script src="https://someone-else.com/thing.js"> in your HTML is a standing instruction to your visitors' browsers: fetch this file from a server you don't control and run it with my page's full permissions. It can read the DOM, read non-HttpOnly cookies, see what users type into your forms, and make requests as the logged-in user. For as long as that tag is in your page, you are trusting whoever controls that host, today and every day after.

So the question isn't "did I write good JavaScript." It's "every host my page loads code from, do I still control what it serves?" There are three ways the answer turns out to be no, and all three are readable from outside, in the HTML you already serve to everyone.

One: the CDN is already known to be hostile

The polyfill.io attack wasn't a one-off. The same operator was tied to bootcdn.net, bootcss.com, and staticfile.net/staticfile.org, a cluster of "free" CDN mirrors that a lot of older tutorials told people to use. When one of those goes bad, every page still pointing at it is serving the payload.

This is the cleanest check there is, because it's a closed list. There's no guessing, no heuristic. If your page loads a script or stylesheet from one of those confirmed-compromised hosts, it fires, critical, every time. No false positives, because the set is fixed and each entry has a public, documented compromise behind it.

$ scan yoursite.com
probing yoursite.com from outside, no credentials...
https://polyfill.io/v3/polyfill.min.jspolyfill.io supply-chain attack, CVE-2024-38526
https://cdn.bootcss.com/jquery/3.4.1/jquery.min.jssame operator cluster, malware-serving
https://cdn.staticfile.org/vue/2.6.14/vue.min.jssame operator cluster
3 exposures visible to anyone. None required a login.
A closed denylist of CDNs with a documented hijack behind each one. If your page loads from one, it runs whatever that host serves now.

The fix is to stop pointing at it. Remove the tag, or self-host a known-good copy of the library you actually wanted. If you genuinely needed a polyfill service, move to a mirror with an operator you trust, like cdnjs, and pin it with a Subresource Integrity hash so the browser rejects the file if even one byte of it changes.

Two: the host doesn't exist anymore

The second case is quieter and, in a way, scarier, because nothing in your page looks broken to you. A script tag points at a third-party host whose domain has lapsed. The dependency is already dead, the feature it powered silently stopped working, and you may not have noticed because it was an analytics beacon or a widget nobody mentions.

Here's the problem. A lapsed domain is a domain anyone can buy. The moment someone registers the name your script tag still points at, they own the file at that URL, and your page hands their JavaScript to every visitor. It's the client-side version of subdomain takeover: a dangling reference that becomes a live attack the day someone claims the other end.

$ yoursite.com
$ # your page still loads this
$ grep -o 'src="[^"]*"' index.html
src="https://old-widget-host.com/embed.js"
$ dig +short old-widget-host.com
(no answer: NXDOMAIN on A and AAAA)
$ whois old-widget-host.com | grep -i status
available for registration
The host hard-fails DNS on both A and AAAA. The dependency is dead, and the name is re-registerable to anyone who wants to serve code as your page.

The check is conservative on purpose. It only flags a host that returns a hard NXDOMAIN on both the A (IPv4) and AAAA (IPv6) records. A timeout or a SERVFAIL doesn't count, because those mean "couldn't reach the nameserver right now," not "this name is gone." Only a clean, authoritative "no such host" trips it, the same gate the dangling nameserver and dead-MX checks use. We phrase the finding as dangling rather than confirmed-takeover-able, because confirming the domain is buyable would need a registrar lookup we don't do, but a script reference to a host that no longer resolves is worth removing either way.

Three: the host is a cloud bucket nobody owns

The third case is a specific, common shape of the second. The script loads from a cloud storage bucket, an S3 URL like mybucket.s3.amazonaws.com or an Azure blob host, and the bucket has been deleted. The host still resolves, because the provider's domain is fine. It's the bucket name underneath that's vacant.

When you GET the root of a deleted bucket, the provider tells you so in plain text: S3 returns NoSuchBucket, Azure returns ContainerNotFound or BlobNotFound. That response is also an invitation. Bucket names on S3 and Azure are first-come, first-served and globally unique, so an attacker who sees that banner can create a bucket with the exact same name, upload their own app.js, and your page starts serving it. Same URL, same trust, attacker's code.

request
GET / HTTP/1.1 Host: mybucket.s3.amazonaws.com
response
HTTP/1.1 404 Not Found
Content-Type: application/xml
<Error><Code>NoSuchBucket</Code>
<Message>The specified bucket does not exist</Message>
</Error>
The bucket is gone and the name is free. Anyone can re-create mybucket and serve their JavaScript from the URL your <script> tag already trusts.

The gate here is multi-signal so it can't misfire: the host has to be S3-shaped or Azure-blob-shaped, the GET has to come back 404 or 400, and the body has to carry the provider's literal missing-bucket banner. Only all three together fire. Google Cloud Storage is deliberately left out, because GCS doesn't let a deleted bucket's name be re-registered for a long retention window, so a missing GCS bucket isn't claimable the way an S3 one is.

The fix for both the dead host and the dead bucket is the same: remove the reference if you don't need it, or, if you do, re-create the resource under your own control and lock the name down. Then, as with every third-party script, pin it with SRI so a swapped file gets rejected instead of run.

Reading it from outside

All three of these are sitting in the HTML you serve to every visitor, which is exactly why a stranger can assess your supply chain without touching your backend, and why SurfaceCheckr can too. We fetch your pages, pull out every cross-origin <script> and <link>, and check each host three ways: is it on the known-compromised list, does it still resolve in DNS, and if it's a bucket, is the name unclaimed. First-party assets are skipped, because the risk is the code you load from someone else.

What we can't tell you is whether a third party's code is doing something hostile right now while it's still under the right owner's control. That part runs in the browser, out of our view, and we won't pretend to see it. What we will hand you is the list of script hosts your page trusts that it shouldn't: the hijacked CDN, the lapsed domain, the empty bucket. For most sites that's a list nobody has looked at since the tags went in, which is the whole problem.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.