← Back

Stateless Anti-Bot Protection for Hidden Services

- Maverick

Software Hidden-Service


I finally finished the stateless CAPTCHA I’ve been obsessing over for the last few weeks. The idea had been rotting in the back of my head while I worked on other things, but the absolute state of anti-bot protections on Tor finally pushed me to write the code.

JS is out of the question. I don’t use it here, and I certainly won’t force Tor users to enable it just to prove they exist. The whole thing runs purely on HTML and CSS. These constraints forced me to get creative. I scrapped the logic three times before getting it right, but the final result works surprisingly well using standard browser features.

If you run a hidden service, you know the pain. Most users are on Tor Browser. The smart ones use the “Safest” security level, which kills JS entirely. If your security relies on scripts, you’re breaking the site for your best users and forcing them to lower their shields just to prove they aren’t robots. That’s hostile design.

Heavy JS over Tor is slow and leaks info. The alternatives aren’t much better. Proof-of-Work systems kill mobile batteries and punish users with old hardware. Waiting 30 seconds to hash a nonce just to post a comment is miserable. Text CAPTCHAs are dead; OCR bots read distorted text better than I do. We need to verify intent and physical interaction, not just character recognition.

I’m heavily inspired by what HugBunter and Paris did with Dread, and Wamus with Pitch. Their rotation puzzles proved you can beat bots by forcing physical interaction, all while respecting the “Safest” mode limitations. I think they are incredibly creative for figuring out how to bypass Tor Browser restrictions with nothing but clever CSS hacks. I wanted that same tactile feel, spinning a circle until it’s upright, so I built my own version from scratch. I also added two other variants, a slider and a pair-matching puzzle, that are also purely HTML and CSS.

Implementing this in pure CSS was a special kind of hell. It uses hidden radio buttons, sibling selectors, and transforms. Clicking the control checks a hidden input, and the CSS picks up that state change to rotate the image. It looks like magic, but it’s really just the checkbox hack doing the heavy lifting.

I wanted this to be stateless from the start. Most systems store session IDs in Redis or Postgres. User submits, you look up the ID. That scales poorly. If Redis dies, your CAPTCHA dies. If you get flooded, memory fills up.

Instead of a database, the server generates a challenge, packs the answer with a timestamp and salt, and encrypts it all into a binary buffer using ChaCha20-Poly1305. This encrypted token goes to the client as a hidden field. The server stores nothing. When the user submits, they send back their answer and the token. The server decrypts the token and compares the answer. If the decryption fails, the token was tampered with.

ChaCha20-Poly1305 beat out AES because it’s fast in software and has built-in authentication. No separate HMAC, less complexity to screw up.

I wrote the core logic in Rust because I didn’t want to deal with memory leaks or slow execution. But a library is useless if it’s hard to use, so I added a C FFI layer so you can call it from almost anything. I included wrappers for Go, Python, and PHP in the examples/ directory so you can drop it into a Django app or WordPress plugin without fighting with bindings.

I built this for myself, but it’s public now because we need options that don’t involve Google or Cloudflare. You can find the code on the clearnet here or via Tor here. This blog is running it right now. If you’re reading this, it works.

Drop a note if you find a way to break the CSS logic. Just don’t ask for JS support.


Related Posts

Why I Build the Way I Build

Goodbye Nextcloud, it's not you, it's my RAM.

How I Turned My Old Phone Into a Mini Homelab