A Russian developer who started with a simple FastAPI todo list four months ago ended up running a multi-product platform and uncovering a dozen security issues after bots flooded the registration form.
The service at getdoday.ru grew to include a planner for schoolchildren, a Q&A module, a traffic-ticket trainer, and a tutor dashboard, all inside one 86,000-line monolith with 1,325 tests. Within two weeks the database showed 238 new accounts; only 62 confirmed their email. The remaining 176 were bots using disposable domains such as sdffsd.sdd and prweorwef.com.
Bots bypassed three protection layers
The original defenses were a honeypot field, a five-registrations-per-minute IP limit, and email confirmation. None worked. The rate limiter used an in-memory deque that was cleared on every deployment; with dozens of commits per day the counter effectively never existed. uvicorn was started with --forwarded-allow-ips='*', causing it to trust the leftmost IP supplied in the X-Forwarded-For header. An attacker could therefore rotate a fake address on every request and never hit the limit. Email verification set a timestamp but the column was never checked anywhere in the authorization logic.
Instead of adding hard email gates that would hurt real users behind school NATs, the developer implemented three lightweight checks: a signed timestamp proving the form was rendered at least a few seconds earlier, hourly registration counters stored in the database with separate per-subnet limits, and a DNS lookup confirming the mail domain actually exists.
Stored XSS via JSON-LD and an IDOR
During the subsequent full audit twelve additional vulnerabilities were found. Public Q&A pages rendered user-supplied question titles inside a JSON-LD script block using the safe filter. Because the HTML parser does not understand JSON, an attacker could close the script tag and inject executable code that exfiltrated the victim’s entire account export.
Another flaw allowed any logged-in user to read every task belonging to a project simply by supplying its UUID to the /api/tasks endpoint; membership checks existed only on the HTML page, not inside the service layer.
Other issues included an inability to invalidate sessions after password change and rate-limit bypasses on multiple endpoints. All findings were addressed with tests and the developer published the complete post-mortem on Habr.


