fix: harden native http protocol boundaries

This commit is contained in:
2026-09-21 02:54:42 +08:00
parent 3d5f225fb2
commit 22939fdb0b
4 changed files with 240 additions and 18 deletions

View File

@@ -479,23 +479,39 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
}
name := strings.TrimSpace(line[:index])
value := strings.TrimSpace(line[index+1:])
if !validToken(name) || strings.ContainsAny(value, "\r\n") {
if !validToken(name) || !validHeaderValue(value) {
return nil, true, &protocolError{status: http.StatusBadRequest, message: "malformed header"}
}
header.Add(name, value)
}
if value := header.Get("Expect"); value != "" {
return nil, true, &protocolError{status: http.StatusExpectationFailed, message: "unsupported expectation"}
if protoMajor == 1 && protoMinor == 1 && (len(header.Values("Host")) != 1 || header.Get("Host") == "") {
return nil, true, &protocolError{status: http.StatusBadRequest, message: "missing host header"}
}
if value := header.Get("Transfer-Encoding"); value != "" && !strings.EqualFold(value, "identity") {
return nil, true, &protocolError{status: http.StatusNotImplemented, message: "chunked requests are not supported"}
for _, value := range header.Values("Expect") {
if strings.TrimSpace(value) != "" {
return nil, true, &protocolError{status: http.StatusExpectationFailed, message: "unsupported expectation"}
}
}
for _, value := range header.Values("Transfer-Encoding") {
for _, coding := range strings.Split(value, ",") {
coding = strings.TrimSpace(coding)
if coding != "" && !strings.EqualFold(coding, "identity") {
return nil, true, &protocolError{status: http.StatusNotImplemented, message: "transfer encoding is not supported"}
}
}
}
contentLength := int64(0)
if value := header.Get("Content-Length"); value != "" {
contentLength, err = strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil || contentLength < 0 {
return nil, true, &protocolError{status: http.StatusBadRequest, message: "invalid content length"}
contentLengthSeen := false
for _, value := range header.Values("Content-Length") {
for _, part := range strings.Split(value, ",") {
part = strings.TrimSpace(part)
parsed, parseErr := strconv.ParseInt(part, 10, 64)
if parseErr != nil || parsed < 0 || (contentLengthSeen && parsed != contentLength) {
return nil, true, &protocolError{status: http.StatusBadRequest, message: "invalid content length"}
}
contentLength = parsed
contentLengthSeen = true
}
}
if contentLength > maxBodyBytes {
@@ -523,8 +539,8 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
RequestURI: target,
RemoteAddr: conn.RemoteAddr().String(),
}
closeAfter := strings.EqualFold(header.Get("Connection"), "close") ||
(protoMajor == 1 && protoMinor == 0 && !strings.Contains(strings.ToLower(header.Get("Connection")), "keep-alive"))
closeAfter := headerHasToken(header.Values("Connection"), "close") ||
(protoMajor == 1 && protoMinor == 0 && !headerHasToken(header.Values("Connection"), "keep-alive"))
request.Close = closeAfter
return request, closeAfter, nil
}
@@ -540,14 +556,43 @@ func validToken(value string) bool {
if value == "" {
return false
}
for _, char := range value {
if char <= ' ' || strings.ContainsRune("()<>@,;:\\\"/[]?={} \t", char) {
for index := 0; index < len(value); index++ {
char := value[index]
if !isTokenByte(char) {
return false
}
}
return true
}
func isTokenByte(char byte) bool {
if char >= '0' && char <= '9' || char >= 'A' && char <= 'Z' || char >= 'a' && char <= 'z' {
return true
}
return strings.ContainsRune("!#$%&'*+-.^_`|~", rune(char))
}
func validHeaderValue(value string) bool {
for index := 0; index < len(value); index++ {
char := value[index]
if (char < 0x20 && char != '\t') || char == 0x7f {
return false
}
}
return true
}
func headerHasToken(values []string, token string) bool {
for _, value := range values {
for _, item := range strings.Split(value, ",") {
if strings.EqualFold(strings.TrimSpace(item), token) {
return true
}
}
}
return false
}
func isTimeout(err error) bool {
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
@@ -614,12 +659,12 @@ func (w *responseWriter) writeTo(conn net.Conn, closeAfter bool, timeout time.Du
w.wroteHeader = true
}
body := w.body.Bytes()
noBody := w.requestMethod == http.MethodHead || w.status == http.StatusNoContent ||
noBody := w.requestMethod == http.MethodHead || w.status == http.StatusNoContent || w.status == http.StatusResetContent ||
w.status == http.StatusNotModified || (w.status >= 100 && w.status < 200)
if noBody {
body = nil
}
if w.header.Get("Content-Length") == "" && w.status != http.StatusNoContent && w.status != http.StatusNotModified {
if w.header.Get("Content-Length") == "" && w.status >= 200 && w.status != http.StatusNoContent && w.status != http.StatusNotModified && w.status != http.StatusResetContent {
w.header.Set("Content-Length", strconv.Itoa(len(w.body.Bytes())))
}
if closeAfter {
@@ -637,8 +682,11 @@ func (w *responseWriter) writeTo(conn net.Conn, closeAfter bool, timeout time.Du
output.WriteString(http.StatusText(w.status))
output.WriteString("\r\n")
for _, key := range keys {
if !validToken(key) {
return errors.New("invalid response header name")
}
for _, value := range w.header.Values(key) {
if strings.ContainsAny(value, "\r\n") {
if !validHeaderValue(value) {
return errors.New("invalid response header")
}
output.WriteString(key)