Site cover image

Site icon image vicevirus’ Blog

Yo, welcome to my blog! I write tech stuff and play CTFs for fun. (still a noob)

🔎 Maybe whitelisting Cloudflare IPs isn’t always a good idea…

Introduction


Few weeks before, AssetNote dropped a fantastic tool called newtowner, which abuses IP whitelisting logic to access internal endpoints, a reminder that trusting source IPs alone is a fragile security model.

A few months back, I went down the Cloudflare IP whitelisting rabbit hole myself, where I uncovered similar flaws that many engineers tend to overlook.

Let’s dig in :)

The issue


Most teams who want to “protect” their backend behind Cloudflare start by copy-pasting the public Cloudflare IP ranges into a whitelist and call it a day. Maybe it’s part of a migration, maybe it’s some Stack Overflow copy-pasta, maybe it just came from the company wiki.

cloudflare_ipv4 = [
  "103.21.244.0/22",
  "103.22.200.0/22",
  "103.31.4.0/22",
  "104.16.0.0/13",
  "104.24.0.0/14",
  "108.162.192.0/18",
  "131.0.72.0/22",
  "141.101.64.0/18",
  "162.158.0.0/15",
  "172.64.0.0/13",
  "173.245.48.0/20",
  "188.114.96.0/20",
  "190.93.240.0/20",
  "197.234.240.0/22",
  "198.41.128.0/17"
]

if client_ip in cloudflare_ipv4:
    allow()
else:
    deny()

On the surface, this feels rock solid.

Requests from “the internet” get blocked, requests from Cloudflare sail right through. ✅

You pat yourself on the back, push to prod, and move on.

This happens very frequently, and even I admit I’ve done it before. People take the published Cloudflare list, drop it straight into their firewall rules or reverse proxy config, and trust it blindly because “hey, it’s from Cloudflare, nothing bad right?”

But it gets even worse

Most teams don’t just apply this logic on endpoints that are supposed to be public, sometimes they copy-paste the same Cloudflare IP check across all routes, including sensitive, internal endpoints.

Image in a image block
Example illustration

Well.. here’s the real problem with this practice:

  • Anyone can route through Cloudflare.

    You don’t need to own the app. You don’t need magic. You just need a free Cloudflare account. There’s zero verification the request came from your Cloudflare tenant, could be me, could be anyone.

  • All you’ve done is move your trust boundary.

    Instead of trusting the internet, you now trust a public network that literally anyone can use <3

  • Abuse becomes much easier.

    If your backend thinks “request from Cloudflare == safe,” it will happily serve internal-only or sensitive endpoints to attackers.

How to Fake Being Cloudflare 🤔


Erm.. so, how do we actually make ourselves look like we’re coming from Cloudflare? (easiest way)

Well, we can use a Cloudflare Worker.

What is a Cloudflare Worker?

A Cloudflare Worker is a lightweight serverless function that runs at Cloudflare’s edge. You can write simple JavaScript code that handles HTTP requests and responses, proxies traffic, or modifies data, all running on Cloudflare’s global network.

Developers use Workers for things like building APIs, proxying requests, rewriting headers, and more. All Worker traffic comes from Cloudflare IP ranges.

I like to say it’s sort of like AWS Lambda but for Cloudflare.

How does Cloudflare worker helps in this case?

Any request that is made by the Worker will be from within Cloudflare IP ranges.

You don’t even need a real account for a very quick testing. Just go to Cloudflare Workers Playground. No login, no setup needed. Write a few lines to proxy requests to your target backend.

Here’s a minimal Worker that proxies traffic to any backend of your choice:

// worker code
// you may adjust this as you please, this should give the idea :)
export default {
  async fetch(request) {
    try {
      const url = new URL(request.url)
      const targetHost = url.searchParams.get('host')

      if (!targetHost) {
        return new Response('Missing ?host=', { status: 400 })
      }

      const res = await fetch(`http://${targetHost}`, {
        method: 'GET',
        headers: {
          'Accept': 'application/json'
        }
      })

      const responseText = await res.text()

      return new Response(responseText, {
        status: res.status,
        headers: {
          'Content-Type': res.headers.get('Content-Type') || 'text/plain'
        }
      })

    } catch (err) {
      return new Response(`Unexpected error: ${err.message}`, {
        status: 500,
        headers: { 'Content-Type': 'text/plain' }
      })
    }
  }
}

What is supposedly to be forbidden and inaccessible

Image in a image block

suddenly becomes.. accessible

Image in a image block

Mitigation


  • Host header validation

    By default, Cloudflare Workers can’t set arbitrary Host headers.

    On your backend, make sure you’re validating the Host header to only accept the domains you actually expect. This blocks most generic Worker relays and open proxies.

  • Authenticated Origin Pulls (mTLS) – This is a good one, but can be a roadblock to devs as it requires setting up and managing client certificates on both Cloudflare and your origin server.
  • Use cf.worker.upstream_zone (if you still somehow want a Worker to access your env)

    You can set this rule to make sure only allow requests from your own trusted Worker domains.

  • Don’t trust source IPs alone

    IP whitelisting by itself is not a security control. Always pair with proper authentication or header validation wherever necessary.