Commit c41a7a8
committed
Evict the oldest /begin row instead of 429 at cap
/login/passkey/begin used to refuse with 429 and Retry-After: 300
once the table held 64 unexpired challenges. Because the
endpoint is unauthenticated, any actor could send 64 POSTs in a
burst and park the table at the cap for the full 5-minute TTL,
which forced every legitimate passkey sign-in into 429 for that
window and turned the anti-bloat safeguard into a trivial
availability DoS.
Switch the at-cap branch from "return 429" to "evict the oldest
unexpired row, then insert." The cap still holds (size never
exceeds 64) but no caller ever sees a 429, so an attacker can't
hold the door shut. Eviction picks the row with the smallest
expires_at, which is the FIFO head since expires_at is just
insert time + TTL.
There's a residual displacement risk: a sustained burst could
race a legitimate user's row out of the table before they reach
/finish. That's a much harder attack than the cap-park one this
fixes, and it lives at the same threat tier (network-bandwidth
abuse) where a reverse proxy is the right defense layer.
The 429 test in src/pages/login.test.ts is replaced with one that
asserts the eviction behaviour: pre-fill the table to 64 rows
with ascending expires_at, hit /begin, and check the response is
200, the table size is still 64, and the oldest seeded row is
gone.
#487 (comment)
Assisted-by: Claude Code:claude-opus-4-71 parent 9682924 commit c41a7a8
2 files changed
Lines changed: 48 additions & 34 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
106 | 110 | | |
107 | 111 | | |
108 | | - | |
109 | | - | |
| 112 | + | |
110 | 113 | | |
111 | | - | |
| 114 | + | |
112 | 115 | | |
113 | | - | |
| 116 | + | |
| 117 | + | |
114 | 118 | | |
115 | 119 | | |
116 | 120 | | |
117 | 121 | | |
118 | 122 | | |
119 | 123 | | |
120 | 124 | | |
121 | | - | |
122 | | - | |
123 | | - | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
124 | 128 | | |
125 | 129 | | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
126 | 135 | | |
127 | 136 | | |
128 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
29 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| |||
265 | 266 | | |
266 | 267 | | |
267 | 268 | | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
273 | | - | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
| |||
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
| 288 | + | |
291 | 289 | | |
292 | 290 | | |
293 | 291 | | |
294 | 292 | | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
295 | 300 | | |
296 | | - | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
297 | 312 | | |
298 | 313 | | |
299 | 314 | | |
300 | 315 | | |
301 | 316 | | |
302 | 317 | | |
303 | | - | |
304 | 318 | | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | 319 | | |
315 | 320 | | |
316 | 321 | | |
| |||
0 commit comments