Stop Cloudflare bypassing on shared hosting

Hey guys, I hope this woks! :wink:

With the recent announcement of the new free tier for Cloudflare Workers, I found no more excuses not to use the full depth of my copy & paste JavaScript programming skills. (Thank you, Cloudflare Workers Recipes authors!)

One of the big problems for Cloudflare users with sites on shared hosting is that hackers may find the IP address and connect directly to it, bypassing Cloudflare and the protections it offers. So here’s a solution involving a Cloudflare worker, a few .htaccess directives, and a Firewall Rule.

With a very simple Cloudflare Worker, we can add a request header, a header that will be sent from the edge (any of Cloudflare’s 180+ data centers) to the origin (your server), and therefore won’t be visible to site visitors. As long as the header name and value are kept secret by the site admin, any requests not coming through Cloudflare will not have this header, and will therefore trigger a rewrite condition at the origin server, and be redirected back to, well, Cloudflare - where a Firewall Rule will block it.

The worker (taken from this recipe):

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

/**
 * Send header to origin, allowing for
 * .htaccess to block requests
 * not coming from Cloudflare
 */

async function handleRequest(request) {
  // Make the headers mutable by re-constructing the Request.
  request = new Request(request)
  request.headers.set('Secret-Header', 'SeCrEt-kEy')

  return await fetch(request)
}

The .htaccess directives (place them at the top of the file):

# Route visitors not coming from Cloudflare to, well, Cloudflare
<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteBase /
	# Both the header and the value should be kept secret
	RewriteCond "%{HTTP:Secret-Header}" "!SeCrEt-kEy"
	# Uncomment and edit w/ IP of services such as certs, cron, Softaculous etc
	# RewriteCond "%{REMOTE_HOST}" "!^xxx\.xxx\.xxx\.xxx$"
	RewriteRule .* "accessdenied.php" [R,L]
</IfModule>

What these directives do is check every request to see if it has a request header named “Secret-Header” and whether its value does not contain the string “SeCrEt-kEy”. If the header does not exist, or does not contain the key, the request will be redirected to a non-existing URI named here “accessdenied.php”, (a fictitious, non-existing file) which must be added in a Firewall Rule:

(http.request.uri.path eq "/accessdenied.php") Then... Block

I’ve seen a similar solution, that tested for the existence of any CF-* header, mentioned a few times in this Community, but a hacker can always add a bunch of phony CF-* headers to the requests performed by their bots.

Also, I prefer to redirect to Cloudflare instead of blocking these requests at the origin because a redirect takes ~200 bytes, while a standard 403 response page from my origin will take about 6x that amount. I could rewrite the 403 page down to a few bytes, but I still want to keep my site functional for legit visitors, and that includes meaningful error pages whenever they face one.

One consequence of this approach is that if you monitor your server logs, you’ll find it may contain both legit 302s as well as, should anyone ever try to access your website via IP address, 302s for the URLs the bot was trying to reach. The Cloudflare Firewall Events log will then have the entries for the blocks executed for URL "/accessdenied.php".

After implementing this solution, you may find that there are 302s on your server log that have no match on the Firewall Events log. This is because bots may be programmed not to follow redirects. It’s important to note that for legit bots accessing directly your site to provide services such as cron jobs, cert renewal etc, there’s the bypass rule on the .htaccess directives above, you just need to uncomment and edit it to include the service’s own IP address.

Cloudflare Workers is a paid service, and costs $5/month for the first 10 million requests, $0.50 per up to 1 million additional requests. (While I’ve tested this setup using the “free tier”, it isn’t advisable to use the free tier on a production website, as it has some limits that, when reached, will make the site break and generate 1015/1025 error pages for the duration of the rate limiting period, something about which site owners have no control.)

I can’t say this hack is going to work for everybody. I’ve tested it many, many times in the past few days (what a royal pain to test htaccess!), but since every host has different configs, firewalls, caching etc and since I just had one platform to test it (shared hosting on Siteground, Apache 2.4.x), I hope you guys will be kind enough to share your feedback if something goes wrong.

Happy Workering!

16 Likes

Can I just make something up like, “bunny?”

That’s the idea! Not only you can, but you should make up a header name that it’s your own, as this will increase the difficulty of guessing it. Just make sure you respect the limits Apache has for header names and values (special and some punctuation characters may not be accepted). The .htaccess as written above will check for the header’s presence, and for its value. The value is case-sensitive, the header name is not. So:

	RewriteCond "%{HTTP:bunny}" "!bunnyIsNotACat"

Will expect a header that could be bunny, Bunny etc, with a value that must match bunnyIsNotACat as spelled. You can test it with the following curl command:

curl -svo /dev/null http://example.com/ --connect-to ::xxx.xx.xx.xx --header "Secret: MySecret"

replacing the values for your origin server IP address and whatever header/value pair you set up on your .htaccess.

1 Like

Thanks for the explanation. Now I have a thought. Would it be possible for someone to see the headers and thus forge it? If so, is it possible to use a math eqation where the Workers code expects a certain value in htaccess? Or is this not required based on what this code does already?

To the best of my knowledge, no. It wouldn’t be possible for someone to see it by just visiting your site, because the header is sent from the Cloudflare edge to your origin server, but not from CF to the visitor.

If the traffic between CF and your origin server is ever compromised, then it could theoretically become visible, but then your site would be vulnerable no matter what protections Cloudflare can offer, right?

What can happen is a scenario where, say, for debugging purposes, you set your .htaccess to echo back all request headers. If you ever do so, and forget to first turn off the secret header, then the key could become visible. Or, a more likely situation, you hire a third party to help you with development, or somehow you give them access to your .htaccess. In any case, you’d need to change the key (the header value).

What you can do to avoid disrupting your website when changing keys is to set the rewrite condition to test for two keys. I believe this would allow for a smooth rotation:

RewriteCond "%{HTTP:Secret-Header}" "!(^current-key|^future-key)"

I didn’t get an email for this response so just seen this. I added your code to Workers and the htaccess code and it works. I even tested with a hosts file edit to my origin and I got a 404 because the file wasn’t there. So I added this code at the end of the htaccess for a 403.

RewriteRule ^ - [F,L]

Would that be alright? Not sure how to use the file part of the htaccess code. The accessdenied.php

1 Like

It’s up to you. The way I wrote the .htaccess code, it will redirect to a non-existing file (hence the 404 you got) that would be then be blocked by a Cloudflare Firewall Rule. So you’d only see a 302 redirect on your server logs, and a block in the Firewall Events log. This way, your server is relieved of having to generate a 403 (or even a 404), which in case of many requests can add to its regular operating load. (Remember, the idea is to stop bots from bypassing Cloudflare.)

If instead you keep the .htaccess as I wrote it, it needs to be coupled with a Firewall Rule, as provided. You need to go to Firewall > Firewall Rules and create a rule (or add this directive to an existing rule):

(http.request.uri.path eq "/accessdenied.php") then... Block

So whoever tries to access your site directly is redirected back to Cloudflare, where it’s sweetly blocked.

Ah, now that makes a lot more sense. Thanks!

1 Like

How can I get this to work with subdomains? I have an A record for my root domain and a cname for the subdomain www., both Cloudflare proxied. Everything works as described when visiting the site via the root domain but using the www subodmain, the cname redirects to the root domain and then gets blocked.

I ensured the worker route is example.com/. Is the Enterprise Plan required to support this?

I don’t know why you are being blocked. I have this set up both for a naked domain that redirects to the www, and the other way around, without any issues.

The route should actually have a wildcard, like example.com/*, see if that fixes it.

And no, no need to upgrade plans. Only Workers is required for this to work, regardless of plan level.

Thanks for the quick response. The editor removed the stars from my last post but I think its working now. My route was originally example.com/* but the editor filtered it out on my previous post, that wasn’t working so I changed it to *example.com/* prior to posting here. It wasn’t working initially but testing it now 12 hours later the star character at the beginning of the domain name appears to have fixed it, it just took some time to take effect.

Thank you for sharing this worker and your clear instructions. I’m new to workers and aside from this little issue it was very easy to set up.

2 Likes

Thank you!!!
Just deployed for AWS worked a treat, all of 15minutes work, I am so thankful for your heavy lifting.

2 Likes