Closed kenballus closed 8 months ago
(attn @neild)
Turns out that some servers ignore extra CRLFs between chunks. Here's a payload that demonstrates that discrepancy:
POST / HTTP/1.1\r\n
Host: a\r\n
Transfer-Encoding: chunked\r\n
\r\n
1\r\n
X\r\n
\r\n
\r\n <-- Go sees this as the end of the request, with body "X"
1\r\n
Y\r\n
0\r\n
\r\n <-- Jetty and Waitress see this as the end of the request, with body "XY"
If there were a way to get Go to see a request line where Jetty and Waitress see a chunk size (and extension), then there might be a way to make a request smuggling attack out of this? Not sure if that's possible.
The simplest patch is probably this:
diff --git a/src/net/http/internal/chunked.go b/src/net/http/internal/chunked.go
index aad8e5aa09..044a79d053 100644
--- a/src/net/http/internal/chunked.go
+++ b/src/net/http/internal/chunked.go
@@ -263,6 +263,9 @@ type FlushAfterChunkWriter struct {
}
func parseHexUint(v []byte) (n uint64, err error) {
+ if len(v) == 0 {
+ return 0, errors.New("invalid empty chunk length")
+ }
for i, b := range v {
switch {
case '0' <= b && b <= '9':
This would just make the connection close after the invalid message, but it wouldn't make the response 400, so it's not an ideal fix on its own.
Change https://go.dev/cl/553835 mentions this issue: net/http: respond with 400 Bad Request for empty hex number of chunk length
Actually, it would respond with 400 Bad Request
when parseHexUint
returns a non-error. Therefore, I think your suggestion is sufficient for this kind of case.
Actually, it would respond with
400 Bad Request
whenparseHexUint
returns a non-error. Therefore, I think your suggestion is sufficient for this kind of case.
(I'm assuming you meant "error" instead of "non-error" here.)
This isn't true. It only closes the connection; it does not respond 400. To see for yourself, build master (current commit is https://github.com/golang/go/commit/8db131082d08e497fd8e9383d0ff7715e1bef478) and run the following simple program:
package main
import (
"fmt"
"net/http"
)
func handle_request(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Hello world\n")
}
func main() {
s := &http.Server{
Addr: "127.0.0.1:8080",
Handler: http.HandlerFunc(handle_request),
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
Then send it a request with an empty chunk size:
printf 'POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n\r\n\r\n' | nc localhost 8080
You should see a valid response:
HTTP/1.1 200 OK
Date: Fri, 05 Jan 2024 00:01:42 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8
Connection: close
Hello world
The only difference between the current behavior and the previous behavior is that now the connection is closed after the invalid message body is processed. This should really be a 400; not just a closed connection. I would appreciate it if someone would reopen this issue.
To clarify, you need to do things explicitly with the current fix, something like this:
func echoHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Received: %s", body)
}
because it's lazy read for http.Request.Body
in Go net/http
, which means that it won't read the request body until you do it in http.HandlerFunc, unlike other HTTP frameworks, such as fasthttp that is early read for fasthttp.RequestCtx.PostBody()
, which by contrast means that it will read the request body and handle errors before fasthttp.RequestHandler is invoked. Therefore, if you use fasthttp
for your example, you will find it works as expected.
On the code level, there is no doubt that we can change the current pattern of net/http
to be like fasthttp
, but I am afraid that it might break something critical somewhere and hurt the backward compatibility of Go badly. To be frank, I'd say the current fix is balanced or cost-effective rather than perfect.
because it's lazy read for
http.Request.Body
in Gonet/http
This is unorthodox, but okay. Thanks for letting me know.
I am afraid that it might break something critical somewhere
I think it's damned if you do, damned if you don't. While changing this behavior would probably break someone's work somewhere, not changing it is probably silently breaking people's expectations now.
To be frank, I'd say the current fix is balanced or cost-effective rather than perfect.
Agreed.
Go version
go version devel go1.22-2e6387cbec Fri Dec 1 18:47:51 2023 +0000 linux/amd64
What operating system and processor architecture are you using (
go env
)?What did you do?
import ( "fmt" "net/http" )
func handle_request(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "request received.\n") }
func main() { s := &http.Server{ Addr: "127.0.0.1:8080", Handler: http.HandlerFunc(handle_request), MaxHeaderBytes: 1 << 20, }
}
GET / HTTP/1.1\r\n Host: whatever\r\n Transfer-Encoding: chunked\r\n \r\n \r\n \r\n