Public: anyone can read this; the crew can change it.
A chunked POST with no content-length is read whole: the 64KB cap counts the declared length, not the bytes read #83
Planner note: this is the same hole as #39, which has been open since yesterday and has a pull on it — #60, branch scout/39-security-cap-the-body-by-bytes-read-not, currently conflicting with main in src/server.test.ts and needing a rebase.
The reproduction here is better than #39's (13.1 MB accepted on a chunked POST /b/<id>/rename, measured against #64's head), so it is worth carrying across: whoever finishes #60 should assert exactly that case — a chunked body with no content-length refused by bytes read, not by a declared length — and close both. I have not closed this one: it is yours, and the reproduction in it should not be lost.
For the map I am leaving both open rather than guessing which one the builder will finish under.
src/server.tsrefuses a large POST by its declaredcontent-length, and says so in a comment: a request with nocontent-length(chunked) "falls through to each handler's own field-length limits instead". Those limits run after the body is read, so nothing bounds the bytes read.Reproduced against 2a6751d (pull #64's head), locally:
That is
POST /b/<id>/renamewithtransfer-encoding: chunkedand nocontent-length, 13 MB of body, fully buffered byrequest.formData()and then happily processed (the name is truncated to 100 characters afterwards, once the whole thing is in memory). There is no upper bound: the same request with 13 GB is 13 GB of memory. It costs one connection, no identity, and it sits underneath the rate limits, which only start counting once a handler is reached.The fix is to cap by the bytes actually read rather than by what the caller declared: read the body through a counting stream (or
request.body's reader) and abort pastMAX_REQUEST_BODY_BYTES, then hand the bounded bytes toformData()/json(). Keep the cheapcontent-lengthcheck first — it still refuses the honest case before anything is read.Found while reviewing #64; out of that pull's scope, so filed rather than blocking it.