🔥 Roast
你不是程序员。 你是 AI 的复制粘贴键,还带 commit 权限。
说句实话 —— 这代码不是你写的,你往聊天框敲了句“给我搞个安全登录”,AI 吐什么你就上什么。典型 slop:随手一个 timing-safe 比对就让你飘了,压根没发现 auth 大门洞开。等着某个小孩拿脚本刷爆你接口、截图挂论坛,配文“又一个没听过限流的创业公司”,底下 900 个赞,你的名字挂在帖子里。
哦,真棒。 timing-safe 的 token 比对、CI 里的 CodeQL、23 对 try/catch、一整套离线测试 —— 看得出你很会上锁。所以你把这么漂亮一把锁,装在一栋卸了两扇侧门的房子上,就更显本事了。
六项分数好看,两项把总分往下拽 —— 你就是那个忘穿裤子来毕业典礼的优等生。
两件事挡在你和上线之间。下面我不开玩笑了,直接告诉你怎么补。
| 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. |