🔥 Roast
Not a developer. An LLM’s copy-paste button with commit access.
Be honest — you didn’t write this, you typed “make me secure auth” into a chatbot and shipped whatever fell out. Textbook slop: a shiny timing-safe compare made you feel like an engineer while auth sat wide open. Some kid loops curl over it, posts “startup that’s never heard of rate limiting,” 900 upvotes, your name in the thread.
Oh, brilliant. Timing-safe token compare, CodeQL in CI, 23 try/catch pairs, a real offline test suite — you clearly know how to lock a door. Which makes it all the more impressive that you fitted that gorgeous lock to a house with two side doors taken clean off the hinges.
Six cells in the green, two dragging the whole score down — you’re the honor student who forgot to wear pants to graduation.
Two things stand between you and a launch-ready score. Here’s where the joking stops and the exact fix starts.
| HighCongrats — you built a free brute-force playground That’s not an auth endpoint — it’s an open bar: free, unlimited, 24/7, serving every stranger who fancies grinding your GitHub Device Flow quota to dust with one while(true). | |
| Evidence | worker/worker.js:1631 — POST /api/auth/device/start worker/worker.js:1650 — POST /api/auth/device/poll proxy GitHub Device Flow with no rate limiter wired |
|---|---|
| Root cause | Unthrottled auth endpoints let an attacker brute-force device codes or burn your Device Flow quota; any public auth surface needs a per-IP + global cap. |
| The fix | const ip = request.headers.get("CF-Connecting-IP");
const n = Number(await env.RL.get(`rl:device:${ip}`)) || 0;
if (n > 10) return new Response("rate limited", { status: 429 });
await env.RL.put(`rl:device:${ip}`, String(n + 1), { expirationTtl: 60 }); |
| Verify | for i in $(seq 1 15); do curl -s -o /dev/null -w "%{http_code}\n" \
https://<worker>/api/auth/device/start; done # expect 429 after the cap |
| HighYou serve other people’s HTML on your own origin and call it a day Your doc-serve responses set Content-Type and stop there — no CSP, no X-Frame-Options, no nosniff. You built a beautiful printing press and skipped the part where the ink doesn’t set the building on fire. | |
| Evidence | worker/worker.js:1616 — doc-serve Response sets Content-Type only; no CSP / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / HSTS |
| Root cause | Author HTML is served from the worker’s own origin with no framing or MIME-sniff protection — a real clickjacking / sniff risk for an HTML publisher. |
| The fix | const headers = {
’Content-Type’: ’text/html; charset=utf-8’,
+ ’X-Content-Type-Options’: ’nosniff’,
+ ’Content-Security-Policy’: "frame-ancestors ’self’",
+ ’Strict-Transport-Security’: ’max-age=31536000; includeSubDomains’,
}; |
| Verify | curl -sI https://<worker>/d/<slug>/v/1 | grep -iE \ ’x-content-type|content-security|strict-transport’ |
| MediumYour .gitignore is one line short of a public apology tour Right now a stray git add . is all that stands between your secrets and a very public commit history. | |
| Evidence | .gitignore:0 — .env / .env* not listed; a secret-bearing dotenv could be committed |
| Root cause | An untracked .env is one `git add .` from leaking every key in it — once it hits history, rotating is the only fix. |
| The fix | # append to .gitignore .env .env.* !.env.example |
| Verify | git check-ignore .env # should print .env |
| MediumAuthor HTML served verbatim — your diary on your own kitchen table It’s “fine” the way leaving your diary open on the kitchen table is fine — because it’s your kitchen, for now. | |
| Evidence | worker/worker.js:1610 — author HTML returned verbatim from R2 (reader comments/logins ARE escaped) |
| Root cause | Self-XSS on the author’s own tenant, not cross-user — acceptable on a single-owner worker where the author is the only writer. |
| The fix | if multi-author publishing is added, isolate each doc to a sandboxed per-author origin. |
| Verify | confirm no shared-origin multi-tenant publishing before scaling. |
| MediumWildcard CORS — door wide open, nothing worth stealing behind it yet Access-Control-Allow-Origin: * is fine while nothing sensitive is on the other side — the moment something is, it’s a liability. | |
| Evidence | worker/worker.js:19 — Access-Control-Allow-Origin: * WITHOUT Allow-Credentials |
| Root cause | Public non-credentialed API shape; fine while nothing sensitive is served, dangerous the instant a response carries user data. |
| The fix | if any response becomes sensitive, replace * with an explicit allowlist — never pair * with Allow-Credentials. |
| Verify | curl -sI <worker>/api/... | grep -i access-control |
| Medium4 mutating routes guarded — great until route #5 ships without it Every write route is guarded today; the trouble is the sixth one nobody remembers to guard. | |
| Evidence | worker/worker.js:1774,1859,1907,1981 — requireUploadAuth guards the 4 mutating routes found |
| Root cause | A 5th mutating route could ship without the guard and nobody notices until it’s abused. |
| The fix | add a default-deny test so any unguarded mutating route fails CI. |
| Verify | add a test asserting every POST/DELETE route calls requireUploadAuth. |
| MediumYour errors are confessing to an empty room console.error plus a terminal tail means you find out things broke from users, not from alerts. | |
| Evidence | worker/worker.js:88 — errors only via console.error + wrangler tail; no error-tracking provider |
| Root cause | You find out about breakage from users, not alerts — uncaught errors vanish the moment you stop tailing. |
| The fix | wire a Workers error tracker (Sentry / Logpush) so uncaught errors surface without tailing. |
| Verify | trigger a handled error and confirm it lands in the tracker. |