A client that stops sending its body after the app has answered is never timed out #130

Open
opened 2026-10-14 18:13:37 +02:00 by clawbot · 1 comment
Collaborator

internal/proxy/request.go, responseReceived (called from responseBody.Read in bodies.go) and stopTimers. When the app answers before the client has sent its whole body, and the app's last bytes arrive together with the end of its answer, responseReceived stops the SWWAF_CLIENT_REQUEST_TIMEOUT and SWWAF_UPSTREAM_REQUEST_TIMEOUT timers. The SWWAF_UPSTREAM_RESPONSE_TIMEOUT timer never started, since the app was never sent the whole request. The transport's goroutine is still reading the client's body, with no read deadline on the connection, and holds the body's lock while it waits. Go's server needs that lock before it sends the answer's headers, so the answer is never sent and the handler never returns. finish, which would set the read deadline, runs only after that.

Repro, at the defaults but SWWAF_CLIENT_REQUEST_TIMEOUT=5s: an app that answers 404 without reading the body, and a client that sends POST / HTTP/1.1, Content-Length: 1048576 and three bytes of the body, then nothing, keeping the connection open. After 10 seconds the client had no answer and there was no request log line. The test waited no longer. By the code, it lasts until the client sends more or closes the connection.

SPEC.md: SWWAF_CLIENT_REQUEST_TIMEOUT is "how long a client may take to send its request line and headers, and then, from the end of the headers, its body", and under "Risks the design has to handle" it "bounds how long a request may take to arrive".

Each such request holds a connection to the client, goroutines and their buffers for as long as the client likes. Apps answer many requests without reading the body, such as a 404 or a 401, so a client can hold many of them, from many addresses, and those already held outlive a ban.

Severity: major.

Model: opus-5-5

`internal/proxy/request.go`, `responseReceived` (called from `responseBody.Read` in `bodies.go`) and `stopTimers`. When the app answers before the client has sent its whole body, and the app's last bytes arrive together with the end of its answer, `responseReceived` stops the `SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_UPSTREAM_REQUEST_TIMEOUT` timers. The `SWWAF_UPSTREAM_RESPONSE_TIMEOUT` timer never started, since the app was never sent the whole request. The transport's goroutine is still reading the client's body, with no read deadline on the connection, and holds the body's lock while it waits. Go's server needs that lock before it sends the answer's headers, so the answer is never sent and the handler never returns. `finish`, which would set the read deadline, runs only after that. Repro, at the defaults but `SWWAF_CLIENT_REQUEST_TIMEOUT=5s`: an app that answers 404 without reading the body, and a client that sends `POST / HTTP/1.1`, `Content-Length: 1048576` and three bytes of the body, then nothing, keeping the connection open. After 10 seconds the client had no answer and there was no request log line. The test waited no longer. By the code, it lasts until the client sends more or closes the connection. SPEC.md: `SWWAF_CLIENT_REQUEST_TIMEOUT` is "how long a client may take to send its request line and headers, and then, from the end of the headers, its body", and under "Risks the design has to handle" it "bounds how long a request may take to arrive". Each such request holds a connection to the client, goroutines and their buffers for as long as the client likes. Apps answer many requests without reading the body, such as a 404 or a 401, so a client can hold many of them, from many addresses, and those already held outlive a ban. Severity: major. Model: opus-5-5
Author
Collaborator

Fix in #135: while the client's body has not arrived whole, the app's whole answer no longer stops the timeouts, so SWWAF_CLIENT_REQUEST_TIMEOUT ends the wait. The client is then sent the app's answer, the connection is closed, and the log line says timed_out. A sibling path, where the client sends more once the app has answered and then stops with less than 256 KiB of its body left, is filed as #136.

Model: opus-5-5

Fix in https://git.eeqj.de/sneak/smallwebwaf/pulls/135: while the client's body has not arrived whole, the app's whole answer no longer stops the timeouts, so `SWWAF_CLIENT_REQUEST_TIMEOUT` ends the wait. The client is then sent the app's answer, the connection is closed, and the log line says `timed_out`. A sibling path, where the client sends more once the app has answered and then stops with less than 256 KiB of its body left, is filed as https://git.eeqj.de/sneak/smallwebwaf/issues/136. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/smallwebwaf#130