You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
keel serve: request handler has no socket timeout; a refused POST that declares a body it never sends holds a thread forever #981
Found by the PR #977 security review (round 1). Predates #977.
Problem
KeelHandler (keel/web/server.py) sets no timeout. Two consequences:
_refuse drains the declared body BEFORE answering, so an unauthenticated client that sends Content-Length: 100 and no body gets no response and holds one ThreadingHTTPServer thread indefinitely. Reproduced in the review: a refused halt with CL 100 and no body never answered.
Idle keep-alive connections block forever in readline.
On a deployment with a declared remote origin (keel serve --external-host), anyone who can reach the port can exhaust threads. The halt itself is not delayed per connection (each connection has its own thread, and an admitted halt answers before draining since #977), but enough held threads would starve every connection, the halt included.
Fix sketch
Set timeout on the handler class (e.g. 30 s), so rfile reads raise socket.timeout and the connection closes. Test: a refused POST declaring a body it never sends is closed within the timeout (patch it short in the test); a halt still answers 200 immediately.
Related, smaller
gates._perform opens with the read-write connect(), which CREATES a missing database: a halt that races a deleted keel.db would leave an empty one behind. A mode=rw URI open (as db.connect_read_only does since #976, without query_only) would refuse instead.
Also from the PR #977 round-2 review (pre-existing): a GET's request body is never drained on the success path, so a GET that sends a body poisons the next request on a kept-alive socket, the same shape as #955 for POSTs. Worth covering together with the socket timeout.
Activity
eaitbrahim commentedon Oct 12, 2026
Also from the PR #977 round-2 review (pre-existing): a GET's request body is never drained on the success path, so a GET that sends a body poisons the next request on a kept-alive socket, the same shape as #955 for POSTs. Worth covering together with the socket timeout.
fix(web): drain a GET's body at the request boundary, not in do_GET (#…