Where did beacon.min.js come from on a static site: analysis of Cloudflare's script auto-insertion

On August 16, a thread on Hacker News rose «Cloudflare silently injects its analytics when you switch nameservers» — 354 points by morning. The author pointed the domain to Cloudflare to serve static from R2, and found in the page source a stray <script> where there was not a single line of JavaScript. Comments were split exactly in half: half said “me too,” half said “this has been known for a long time, you turned on the proxy yourself.”

Let’s break it down — because the thread title is formally inaccurate, and the mechanism is described in Cloudflare’s documentation and reproducible in a minute.

Symptom

In the HTML served to the browser, a tag appears just before the closing </body> that isn’t present in the site’s source:

<script defer src="https://static.cloudflareinsights.com/beacon.min.js"
        data-cf-beacon='{"token":"..."}'></script>

Side effects by which this is usually noticed: in the browser console with Enhanced Tracking Protection enabled you get a Cross-Origin Request Blocked, and sites with strict CSP fail the script due to policy. The author of the analysis on burgeonlab writes that the script lived on two of his three domains for about five months and he noticed it only from these console errors.

It is checked with a single command — it is important to look at what edge serves, not what sits on disk:

curl -sL https://example.com | grep -o 'cloudflareinsights[^"]*'

Cause

This is Cloudflare Web Analytics, aka RUM. In the documentation in the section Enabling Cloudflare Web Analytics it states explicitly: for sites proxied through Cloudflare, automatic installation is enabled by default, and the beacon script is inserted without any action by the site owner. In the FAQ the scale is clarified: automatic installation “inserts a JS snippet on all pages (including subdomains) within the zone.”

Important:

A key clarification missing from the HN thread title: it’s not about switching NS servers per se. Insertion is possible only where Cloudflare terminates HTTPS and holds the response body — i.e., when the DNS record is set to “orange cloud” (Proxied). A zone in DNS only mode serves the client the IP of your server, the HTML goes past Cloudflare, and nothing can be appended to it.

The confusion arises because when you add a site, Cloudflare by default offers proxied mode — so for many people “I only changed NS” and “I turned on the proxy” happen in one move.


Diagram based on the Cloudflare developers documentation — Web Analytics and DNS proxy status sections

beacon.min.js — not the only resident

Since we’re talking about rewriting HTML at the edge, it’s worth knowing the full list of what can be appended there. All of these features are documented and have a switch, but their default states differ:

Feature What it does to HTML Enabled by default
Web Analytics (RUM) inserts beacon.min.js before </body> yes, for proxied zones
Email Address Obfuscation replaces email addresses and loads email-decode.min.js yes, “automatically on registration”
Rocket Loader rewrites <script> tags, deferring JS execution no, manually enabled
Note:

Email Address Obfuscation is the most underrated item on the list. It doesn’t just insert a script, it changes the markup: the address becomes <a href="/cdn-cgi/l/email-protection#...">. If your page’s email is parsed by an external service or tested by a test — here’s the reason your local environment is green but production isn’t.

The remedy

Disable this at the account level, not the zone, and it’s not where you’d expect:

Success:
  1. Cloudflare dashboard, account level (not per site).
  2. Analytics & Logs → Web Analytics.
  3. For the relevant domain — Manage site.
  4. Choose the mode and click Update.

There are three modes per the docs:

  • Enable — insertion works (there’s a separate option with EU visitor exclusion);
  • Enable with JS Snippet installation — Cloudflare does not insert anything, you install the snippet yourself;
  • Disable — no insertions.

There’s a second, ancillary path: per the docs automatic insertion does not occur if the origin serves the header Cache-Control: public, no-transform — in this case the proxy is not allowed to modify the payload. This approach works, but it’s treating the symptom via the site’s cache policy; changing it just for one script is a bad exchange. A radical option is to move the record to DNS only, but then the WAF, cache, and DDoS protection, which Cloudflare usually provides, will also go away.

Warning:

What will almost certainly break along the way if you have a Content Security Policy in place: Cloudflare’s automatic insertion requires connect-src to include 'self' (data goes to the same domain), and when installed manually you need cloudflareinsights.com in connect-src, and in script-srchttps://static.cloudflareinsights.com/beacon.min.js. So strict CSP plus auto-insertion results in either console errors or a relaxed policy. There is no third option.

Security:

Separately, there is a legal layer, and it isn’t solved by ticking a box. If you’re handling EU visitors and built a cookie banner for a specific set of scripts, the script appearing on pages without your knowledge did not belong in that set. Cloudflare offers a mode “Enable, excluding visitor data in the EU,” and in a contentious situation that mode, not “Disable,” usually proves to be the compromise that doesn’t break your own analytics. But checking what exactly leaves your pages is the site owner’s task, and no proxy will cover it for you.

What’s truly news here

Nothing, except the scale of surprise. The feature is described in public documentation, can be disabled in four clicks, and is neither a bug nor a vulnerability. The news is that the model of “opt-out by default” at the infrastructure level is visible only to those who inspect their own page’s source, and on HN 354 points, that group is smaller than it seems.

A practical takeaway that applies to any provider standing between your server and the visitor: after every hosting change, CDN, or plan, take a moment to compare curl output for your domain with what’s in your repository. Five seconds of work, and you’ll sometimes find something unexpected. In the same spirit of “check what your production actually serves” — see the analysis of holes in tl;dv and the Firestore Rules checklist.

Sources

Question:

Check your domains right now — interesting to see how many beacon has been hanging for years. And a broader question: where do you draw the line for CDN-permitted behavior? Rewriting a <script> for speed is fine; adding your own script is not? Or is there no difference if you’ve given it your TLS?