DEV Community

Katia Belokon
Katia Belokon

Posted on

How to detect a CDN when your scanner runs on Cloudflare (the Server header won't tell you)

My CDN detector said Cloudflare. Then Cloudflare. Then Cloudflare. Three sites, three identical answers, and the reason was not the sites: the detector trusted the Server header, and it was reading that header from inside Cloudflare.

TL;DR

  • The scanner runs on Cloudflare. From there, every site it fetched came back with server: cloudflare, so it reported Cloudflare on 3 sites out of 3.
  • curl from an ordinary machine shows something else: github.com answers Server: github.com, BBC answers Server: BBC-GTM.
  • Server is not a CDN field. To detect a CDN, use DNS (NS and CNAME) plus headers the CDN documents as its own, and count those headers only when you are not fetching from inside that CDN.

Three for three

I built MyIPScan, a site with free network and privacy checks. One of them takes a domain and reports what it can see from the outside: HTTPS, redirects, security headers, DNS, and a line called "CDN".

The scanner found Cloudflare on three sites out of three.

Cloudflare is big. Three out of three should still make you suspect the detector before the sites.

Why a wrong CDN label matters

People use that label to answer practical questions. Who terminates my TLS? Whose IP ranges do I allow in the firewall? Who do I contact about a block page? Is my origin hidden?

If the tool says "Cloudflare" for everyone, every one of those answers is wrong. A detector that never says "no" is worse than no detector.

The usual suspects, and why they don't fit

The first things you'd suspect are the boring ones: the scan followed a redirect to some third-party host that really is on Cloudflare, or the code parsed the header wrong (case, merged values, a default leaking in).

Neither explains a result that is identical for unrelated sites. The value was the plain string cloudflare, the same every time. That pattern points at the place the request was made from, not at the sites.

The Cloudflare server header, seen from two places

So ask the same question from a machine that is not on Cloudflare. Plain curl, only the line that matters:

$ curl -sI https://github.com | grep -i '^server'
Server: github.com

$ curl -sI https://www.bbc.com | grep -i '^server'
Server: BBC-GTM
Enter fullscreen mode Exit fullscreen mode

Same header name, different answers. From the scanner, both said cloudflare. From outside, neither did.

The scanner runs on Cloudflare, the same platform that serves the rest of the site. I have not found a Cloudflare document that describes what the Server header looks like on responses fetched from there, so I won't guess at the mechanism. The observed part is enough: from inside Cloudflare, server: cloudflare appeared on responses from sites whose Server header, fetched from outside, says something else.

BBC: a CDN that Server never mentions

BBC shows the other half of the problem. A public urlscan.io scan of a www.bbc.com page from 17 July 2026 shows the main response with server: BBC-GTM and via: 1.1 BBC-GTM, 1.1 Belfrage, 1.1 varnish, served from an IP in AS54113, which urlscan labels Fastly.

So that response came through Fastly, and Server did not say Fastly. That is not the header misbehaving. Server names server software, and it never promised to name a CDN. It just can't be used to detect one, in either direction.

How to detect a CDN reliably

A CDN has to be wired into a domain somewhere public. There are two such places: DNS, and the headers the CDN documents as its own.

NS records. Cloudflare's docs say standard nameservers "follow the pattern <proper_name>.ns.cloudflare.com", and advanced nameservers are hosted on foundationdns.com, foundationdns.net and foundationdns.org. If a zone's NS records point there, Cloudflare runs that zone's DNS.

CNAME records. A hostname put behind a CDN often has a CNAME to the CDN. Fastly's docs list targets such as <something>.customer.fastly.net, <something>.map.fastly.net and dualstack.e.sni.global.fastly.net.

CDN-owned headers. Cloudflare says it adds Cf-Ray and Cf-Cache-Status to the response sent to the visitor. Fastly documents X-Served-By, "set by Fastly by default on all responses that we process", in the format cache-{datacenter}{nodeid}-{datacenter}.

None of these is perfect alone. A Cloudflare zone can proxy only some hostnames. An apex served by A records has no CNAME. Headers can be stripped. And a CDN-owned header read from inside that same CDN has the same weakness as Server: it may describe your own path, not the site.

That gives the rule I adopted after this bug: detect a CDN by DNS (NS, CNAME) and the CDN's own headers, never by Server.

A sketch in code

This is an illustration, not the production code, and I have not run it against live DNS. It uses Cloudflare's DNS-over-HTTPS JSON API and returns evidence rather than a verdict:

const NS_RULES = [
  { cdn: 'Cloudflare', re: /\.ns\.cloudflare\.com\.?$/i },
  { cdn: 'Cloudflare', re: /\.foundationdns\.(com|net|org)\.?$/i },
];

const CNAME_RULES = [
  { cdn: 'Fastly', re: /\.fastly\.net\.?$/i },
];

const HEADER_RULES = [
  { cdn: 'Cloudflare', test: h => h.has('cf-ray') },
  { cdn: 'Fastly', test: h => /^cache-/i.test(h.get('x-served-by') || '') },
];

async function doh(name, type) {
  const r = await fetch(
    `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=${type}`,
    { headers: { accept: 'application/dns-json' } }
  );
  const j = await r.json();
  return (j.Answer || []).map(a => a.data);
}

// Naive: right for www.bbc.com -> bbc.com, wrong for www.example.co.uk
// (it returns co.uk). Use a Public Suffix List library in real code.
function apexOf(host) {
  return host.split('.').slice(-2).join('.');
}

// vantage: the CDN your scanner itself runs on, e.g. 'Cloudflare', or null.
export async function detectCdn(host, headers, vantage = null) {
  const evidence = [];

  for (const ns of await doh(apexOf(host), 'NS'))
    for (const r of NS_RULES)
      if (r.re.test(ns)) evidence.push({ cdn: r.cdn, via: `NS ${ns}` });

  for (const c of await doh(host, 'CNAME'))
    for (const r of CNAME_RULES)
      if (r.re.test(c)) evidence.push({ cdn: r.cdn, via: `CNAME ${c}` });

  for (const r of HEADER_RULES) {
    if (r.cdn === vantage) continue; // our own CDN's headers prove nothing here
    if (r.test(headers)) evidence.push({ cdn: r.cdn, via: 'CDN header' });
  }

  // Deliberately no rule for the Server header.
  return evidence;
}
Enter fullscreen mode Exit fullscreen mode

Two details matter more than the regexes.

First, there is no rule for server. It can go into a report as raw data, but it never produces a CDN label.

Second, the vantage check. If the scanner runs on Cloudflare, a cf-ray on a fetched response is skipped, and a Cloudflare label has to come from DNS. NS answers do not have this problem: a zone's nameservers are the same whoever asks. (CNAME and A answers can vary by resolver location, so treat them as evidence, not proof.)

How to check a detector like this

Use the same comparison that exposed the bug: run the detector, then run curl -sI and dig NS / dig CNAME from a machine outside the CDN your scanner lives on. Every label should be backed by something you can reproduce from there. If the only support is a header, and your scanner sits on that CDN, the label should not exist.

The rule

Before you trust what a response says about where it came from, ask where you are standing when you read it. A header describes the path, and your own infrastructure is part of the path. DNS describes the domain.

The CDN and origin hints in the Website Exposure Scanner on MyIPScan are best-effort signals, not proof, and the page says so. (I built MyIPScan.)

Has your monitoring ever reported something that turned out to be a fact about the machine it ran on? What gave it away?

Sources: Cloudflare HTTP headers, Cloudflare nameservers, Fastly X-Served-By, Fastly: routing traffic, urlscan.io scan of a www.bbc.com page.

Top comments (1)

Collapse
 
cdnca profile image
CDNCA •

The vantage-point bug is the real lesson — a detector that reads Server (or even cf-ray) from inside Cloudflare will label almost everything Cloudflare, while curl from outside shows github.com / BBC-GTM and DNS still tells the truth. Treating Server as "CDN software" was never what that header promised, and skipping your own platform's headers when you are the path is the right rule.

Peer note: I work on CDNCA, an open-source CDN control plane. CDN nodes and a plugin marketplace sit on one control plane, and marketplace features roll out release by release. For anyone building edge tooling, "where am I standing when I read the response" is the same discipline as not trusting a HIT badge without knowing which node answered.

After the fix, are you storing the evidence strings (NS / CNAME / header) next to the label so a user can reproduce the claim from outside, or only the final CDN name?