code.backwater.systems/blog/
Backwater Systems<br>blog
If you’ve ever looked at the traffic logs of a website server, you’ve probably marveled at just how much garbage flows through. Now that the “Dead Internet Theory” has been confirmed, this type of traffic is undoubtedly on the rise. For my site, a majority of the spurious requests match these patterns …
wp-admin/
wp-content/
wp-includes/
.env*
*.php
Perhaps someone out there knows something about the tool / botnet that’s responsible? It’s clearly probing for server software exploits and inadvertently exposed credentials. The real-world analogue that I like to imagine is a constant stream of strangers traipsing onto your property to rattle the locks on your windows and doors, muttering words like “loot” and “extortion” while their eyes rove greedily around your valuables. Would you tolerate that? I don’t like the noise drowning out the legit traffic in my server logs; nor do I like thinking about the energy wasted on these attacks. And I do mean wasted, because this site doesn’t run WordPress or PHP at all. I suppose I could accidentally upload an .env file; hopefully not. Although it’s probably an unwinnable war, I decided I won’t stand by and abide it. I shall reward the poor manners of these dull guests of mine with their very own IP address ban! 😈
Initially, I manually scanned the request logs, noted the IP addresses that were sending malicious traffic, and added them to an IP list that a WAF custom rule referenced to block requests from those IPs. That was effective, and satisfying at first, but the 5 to 10 minutes a day I was spending on it became tiresome. The IP addresses changed regularly; I didn’t feel like I was gaining ground. At some point, I came up with the idea to use another custom rule to block requests to those certain paths that the exploit scans routinely tried, using this expression …
(starts_with(http.request.uri.path, "/wp-admin/")) or (starts_with(http.request.uri.path, "/wp-content/")) or (starts_with(http.request.uri.path, "/wp-includes/")) or (ends_with(http.request.uri.path, ".env")) or (ends_with(http.request.uri.path, ".php"))<br>This was also fairly effective, preventing many requests from triggering my infrastructure wastefully. The attacks didn’t slow, tho, things slipped through the defenses, and I was left feeling still vengeful. I wanted to swiftly, automatically outright block the IP addresses originating these attacks. Introducing: IP Address Banhammer™ Cloudflare Worker ! It intercepts the requests to your domain with the purpose of automatically banning IP addresses that request paths matching a configurable list of Regular Expressions. The number of strikes before a ban is also configurable. See the README for instructions to deploy yours today!
This was a quick, one-day project, so things could be optimized and improved—but as-is, it’s effective against the attacks. I feel I can rest a moment from toil. The most interesting part of development was selecting an implementation for the strike counter. My initial choice was Durable Objects; it seemed to be a good use-case fit, and I’d been curious to try it for some time. After starting down that path, I second-guessed whether the increased complexity and costs were worth it for something so simple, and so, “Perhaps the devil ye know?” I quickly refactored the data store to be Workers KV, but it didn’t take long decide against that as well—its characteristics weren’t well-suited to the problem.
Stopping to think—the ideal, uncomplicated implementation for one counter per IP address is perhaps a singleton map with IP address as the keys and the counter as the values. Normally, I would implement a singleton in JavaScript with a module-scoped variable, which conveniently works because modules are only executed once by JavaScript runtimes. The problem with that is that Cloudflare Workers are invoked by the HTTP request cycle, potentially distributed across many different locales, so your normal notion of how program state works isn’t quite accurate. Hmm, let’s check exactly what the documentation says about using global state, tho …
[I]t is generally advised that you not store mutable state in your global scope unless you have accounted for [the] contingency [that a] given isolate has its own scope, but isolates are not necessarily long-lived.
Because there is no guarantee that any two user requests will be routed to the same or a different instance of your Worker, Cloudflare recommends you do not use or mutate global state.
Those warnings are a bit soft … definitely don’t say “never do this!” or “this won’t work at all hahaha.” 🤔 Let’s consider the failure mode to help us decide whether to break the rules. Say partway through an attack, requests were routed to a different data center … the Worker instance (“isolate”) there would have its own map where that IP address had zero strikes again, meaning the attacker would potentially get some bonus requests in...