Leveraging Cloudflare Workers to prevent attackers from bypassing your Cloudflare WAF

SD

Sandrino Di Mattia / July 31, 2020

6 min read––– views

Introduction#

Over the past few years the popularity and usage of Cloudflare has seen a tremendous growth, really living up to their mission of building a better Internet by making it faster and more secure.

Cloudflare Overview

And while Cloudflare adds a lot to the table from a security point of view with their Bot Detection, WAF, DDoS Protection, ... there's still a chance that all of these protections can be bypassed by attacking the Origin IP directly. This is not an issue with Cloudflare itself, but with how the actual web applications have been configured to accept traffic. Tools like CrimeFlare try to map Cloudflare protected sites to their original IP address.

Cloudflair Bypass

Ideally your applications should only accept traffic from Cloudflare, but this introduces some level of complexity depending on the implementation. There's also a lot of content available today describing techniques on how to bypass Cloudflare and find unprotected web applications where the Origin IP can be attacked:

Existing Solutions#

There's several solutions documented which will make it harder for attackers and in some cases will really solve the issue.

Keeping the Origin IP hidden#

A lot of content suggests doing everything in your power to hide the Origin IP, like avoid having DNS record pointing to your IP address directly, being careful with MX records, disabling XML-RPC Pingback, ...

Security trough obscurity at its best and not a true solution.

Only accepting traffic from Cloudflare#

An other type of advice that is commonly given is restricting access to your application based on IP address ranges, only allowing traffic originating from Cloudflare:

Even though this makes things slightly harder for attackers, it still doesn't solve the issue. Nothing prevents an attacker from also using Cloudflare and attacking your Origin IP through Cloudflare. Your servers would not be able to distinguish traffic coming from your Cloudflare account vs. traffic coming from the attacker's Cloudflare account.

This also introduces some complexity: you'll need to setup some automated tooling which will constantly monitor Cloudflare's IP Ranges and change your firewall rules to make sure you don't block new IP addresses used by Cloudflare.

Authenticated Origin Pulls#

Authenticated Origin Pulls give you the guarantee that the request originates from Cloudflare and has been processed by the WAF.

Authenticated Origin Pulls

This capability is based on TLS client certificate authentication and will require some changes to your web server. This is also not always an option depending on where you host your application (PaaS and Serverless environments for example):

nginx.conf
ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
ssl_verify_client on;

The reason why this feature does not solve the bypass problem is because it guarantees that traffic comes from Cloudflare, but there are no guarantees that the request originated from your Cloudflare account. This is because your web server will only validate that the client certificate is signed by the Cloudflare CA chain.

Cloudflare Argo Tunnel#

The best solution to this problem is Argo Tunnel, which creates an encrypted tunnel between your web server and your Cloudflare account. With this feature, you don't have to make your application publicly available on the internet.

Cloudflare Argo Tunnel

This solution does come at an extra cost (including a cost per GB) and at might not work in all environments. Argo Tunnel currently requires to run an agent on your web server:

cloudflared tunnel --hostname www.acme.com http://localhost:8000

Running an agent like cloudflared might not be an option if you don't have full access to your hosting environment (PaaS and Serverless platforms).

An alternative using Cloudflare Workers#

There's an alternative solution similar to Authenticated Origin Pulls which we can build using Cloudflare Workers. By using Cloudflare Workers, we can change the request before it is forwarded to the origin.

What this allows us to do is to configure a secret on both sides (Cloudflare and the web application). The web application can then decide to only accept requests for which the secret is provided (in a header for example). This could be a static value, a HMAC signed request, a JWT ... In this example we're going to secure our origin using a shared secret:

Shared Secret

Let's start by creating a worker which will add an additional header to the request:

function handleRequest(request) {
  const newRequest = new Request(request);
  newRequest.headers.set('x-cf-key', CF_SECRET);
  return fetch(newRequest);
}

addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

The CF_SECRET will be replaced with an environment variable which we can configure on the worker. This setting will contain our shared secret:

Worker Secret

As a final step I'm going to enable the worker for the domain on which my application runs:

Enable Worker

My application runs on Heroku (https://hello-world-sandrino.herokuapp.com/) and is also proxied through Cloudflare (https://hello.sandrino.run). I've created a simple page which lists all of the request headers and for any request served through Cloudflare we can now see the additional header with the shared secret:

Secret forwarded in a header

Note: You will want to treat this header as a secret. It should not be leaked to your users.

Enforcing the header to be present#

The final step is to make sure requests are only served when a X-CF-KEY header is provided and it contains a valid shared secret. In the example below I'm illustrating how this can be done using a middleware in Express, but this can be done in virtually any framework or web server. Most of these allow you to run logic before requests are served and that's the point where you would validate the presence of this header.

server.js
const app = express();

...

app.use((req, res, next) => {
  const key = req.get('x-cf-key');
  if (key !== 'a618e605-fd2d-4e0e-8fe0-139a41193945') {
    return res.sendStatus(404);
  }

  return next(null, req, res);
});

app.get('/', (req, res) => {
  res.render('homepage');
});

Here's the final result. Requests made through Cloudflare are accepted and served as expected because Cloudflare will provide a valid X-CF-KEY header to our web application. Requests that are made directly to the web application will fail because the header is not present.

Enforcing the header

✅ Success!#

Through the use of Cloudflare Workers we can guarantee that the request originates from your Cloudflare account which can be used as a mechanism to prevent bypassing of your Cloudflare configuration. This is a valid solution in case you cannot leverage Argo Tunnels in your environment.

If possible you should configure Cloudflare account to use Full (strict) SSL/TLS encryption and Authenticated Origin Pulls for improved security and to prevent MITM attacks.

Discuss on Twitter