A Captcha Service Where the Success Path Cannot Run

captcha is a public Python repository of mine under github.com/yethikrishna. It is one file, main.py, 129 lines, using FastAPI, Pillow, Redis and a rate limiter. I read the whole file. I did not run it, so what follows is a reading and not a test.

What it does

The /getcap endpoint draws a 200 by 75 image with a random number between 111111 and 999998, two horizontal lines and 1000 randomly colored noise pixels. It stores the number in Redis under a fresh UUID for 65 seconds and returns the JPEG with the UUID in an X-Captcha-Key header. The /verify endpoint takes that key and an answer, compares it to the stored number, deletes the key so it cannot be reused, and issues a 128 character token that lives in Redis for an hour. A third endpoint, /verifytoken, checks that a token exists. Each endpoint has its own rate limit, one request per three, two and one seconds.

Where it goes wrong

The request model declares answer as an int. After a type check that is always true for an int, the code runs len(answer1) != 6. Python raises a TypeError for the length of an int, so by my reading every request that reaches that line fails with a server error before Redis is even asked. The branch that deletes the key and mints a token comes after it. The cheap early checks for a 36 character key are fine, but the real path looks unreachable.

There are smaller points. The number and the token come from Python's random module, which is not meant for secrets, and the secrets module exists for this. The token is only looked up in /verifytoken, never consumed, so it stays valid for the full hour for any number of uses. I also did not see the limiter library initialized in the file, and I did not check whether the version it needs requires that step.

What I would change

My opinion: make the answer a string, check its length and digits, and compare strings. Use secrets for the number and the token. Decide on purpose whether a token is single use. Then write one test that fetches a captcha from a fake Redis, solves it and expects a token, because that is the one flow the service exists for. I wrote this repo quickly and I would not call it ready for real traffic.

← back to the journal