A small URL shortener where every security control has a test
A URL shortener is a good small target for security practice: it takes untrusted URLs, redirects people to them, and has per-user data. I kept secure-shortener to about 250 lines so every control could have a test named after its OWASP Top 10 (2021) category.
Controls
| Category | Control |
|---|---|
| A01 Access control | Every link query uses WHERE owner=?; missing and not-yours both return the same 404. |
| A02 Crypto | scrypt password hashes; 256-bit random tokens stored as SHA-256; 8-character codes from secrets. |
| A03 Injection | SQLite parameter binding everywhere, plus a code-format check. |
| A04 Design | Token bucket of 30 burst at 1/s per IP, returning 429 with Retry-After. |
| A05 Misconfiguration | CSP, HSTS, X-Frame-Options DENY, nosniff; /docs disabled; no server header. |
| A07 Auth | 12+ character passwords, 5 logins per minute per user and IP, the same error for unknown user and wrong password, and a dummy hash so both paths cost the same. |
| A10 SSRF / redirects | Only http and https; blocks loopback, RFC 1918, link-local (cloud metadata), IPv6 loopback, user:pass@ and self-loops. |
Testing it
pytest runs 24 tests in 1.3 seconds. I also ran a smoke script against a live uvicorn server:
- Create returned 201 and the redirect returned 302.
- Reading another user's link returned 404.
http://169.254.169.254/latest/meta-data/andjavascript:alert(document.cookie)both returned 400.- Login brute force returned 401 four times, then 429.
- A burst of 40 requests hit its first 429 at request 12, because earlier smoke calls had already used part of the bucket.
Writing the test name first ("which OWASP category does this belong to?") made it obvious when a control had no test, which is the gap I wanted to close.