🐛 do not drop bodyless DELETE over HTTP/3

Fix #385.

Over HTTP/3 a bodyless request cannot be told apart from one carrying an
unreadable body. Go's HTTP/2 server can make that distinction -- it only sets
ContentLength to -1 when the client left the stream open for DATA frames, so a
request that ends at the headers keeps ContentLength 0 and isBodyUnreadable
stays false (x/net/http2/server.go, `bodyOpen := !f.StreamEnded()`). quic-go has
no equivalent: http3/server_conn.go assigns req.Body unconditionally and
http3/headers.go defaults ContentLength to -1 whenever the Content-Length header
is absent, so every bodyless HTTP/3 request looks unreadable regardless of
method.

With DELETE in isMethodWithBody, that turns an ordinary
fetch(url, {method: "DELETE"}) into a 403 on HTTP/3 while the same call succeeds
on HTTP/2. dani reported this on #352 before it was merged.

A DELETE body is legal but has no defined semantics (RFC 9110 9.3.5), and the
gRPC streams #323/#332 guard against are always POST, so dropping DELETE from
the list costs no protection there. Note this diverges from lua-cs-bouncer's
METHODS_WITH_BODY, which still lists DELETE; the same false positive likely
applies there.

Adds Test_appsecQuery_unreadableBodyMethods, which pins the behaviour for all
seven methods and fails on the previous code for DELETE. isMethodWithBody had no
direct coverage before.
This commit is contained in:
mhx
2026-09-04 11:01:17 +02:00
parent 23ce76d3da
commit 7c96810d3b
2 changed files with 64 additions and 1 deletions
+1 -1
View File
@@ -746,7 +746,7 @@ func isBodyUnreadable(httpReq *http.Request) bool {
// isMethodWithBody used only when isBodyUnreadable returns true but the request method can't have body.
func isMethodWithBody(method string) bool {
switch method {
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
case http.MethodPost, http.MethodPut, http.MethodPatch:
return true
default:
return false