Files
fasthttp/headerscanner.go
Erik Dubbelboer a95a1ad11c Only normalize pre-colon whitespace for HTTP headers (#2172)
Keep headerScanner strict so malformed MIME header lines are still rejected.

Move trimming before ':' into the HTTP header handling paths that
intentionally normalize header names, and add a fuzz seed for the
regression case.
2026-04-04 04:24:04 +02:00

180 lines
3.3 KiB
Go

package fasthttp
import (
"bytes"
"errors"
"fmt"
)
type headerScanner struct {
initialized bool
b []byte
r int
key []byte
value []byte
err error
}
func (s *headerScanner) next() bool {
if !s.initialized {
if bytes.HasPrefix(s.b, strCRLF) {
s.r = 2
return false
}
i := bytes.Index(s.b, strCRLFCRLF)
if i < 0 {
s.err = ErrNeedMore
return false
}
i += 4
s.b = s.b[:i]
if len(s.b) > 0 && (s.b[0] == ' ' || s.b[0] == '\t') {
s.err = errors.New("invalid headers, headers cannot start with space or tab")
return false
}
s.initialized = true
}
kv, err := s.readContinuedLineSlice()
if len(kv) == 0 {
s.err = err
return false
}
// Key ends at first colon.
k, v, ok := bytes.Cut(kv, strColon)
if !ok {
s.err = fmt.Errorf("malformed MIME header line: %q", kv)
return false
}
if !isValidHeaderKey(k) {
s.err = fmt.Errorf("malformed MIME header line: %q", kv)
return false
}
// Skip initial spaces in value.
v = bytes.TrimLeft(v, " \t")
s.key = k
s.value = v
if err != nil {
s.err = err
return false
}
return true
}
// readLine reads a line from b, starting at s.r, and returns it.
func (s *headerScanner) readLine() (line []byte) {
searchStart := 0
for {
if i := bytes.IndexByte(s.b[s.r+searchStart:], '\n'); i >= 0 {
i += searchStart
line = s.b[s.r : s.r+i+1]
s.r += i + 1
break
}
searchStart = len(s.b) - s.r
}
if len(line) == 0 {
return nil
}
// drop \n and possible preceding \r
if line[len(line)-1] == '\n' {
drop := 1
if len(line) > 1 && line[len(line)-2] == '\r' {
drop = 2
}
line = line[:len(line)-drop]
}
return line
}
// readContinuedLineSlice reads continued lines from b until it finds a line
// that does not start with a space or tab, or it reaches the end of b.
func (s *headerScanner) readContinuedLineSlice() ([]byte, error) {
line := s.readLine()
if len(line) == 0 { // blank line - no continuation
return line, nil
}
if bytes.IndexByte(line, ':') < 0 {
return nil, fmt.Errorf("malformed MIME header: missing colon: %q", line)
}
// If the line doesn't start with a space or tab, we are done.
if len(s.b)-s.r > 1 {
peek := s.b[s.r : s.r+2]
if len(peek) > 0 && (isASCIILetter(peek[0]) || peek[0] == '\n') ||
len(peek) == 2 && peek[0] == '\r' && peek[1] == '\n' {
return trim(line), nil
}
}
mline := trim(line)
// Read continuation lines.
for s.skipSpace() {
mline = append(mline, ' ')
line := s.readLine()
mline = append(mline, trim(line)...)
}
return mline, nil
}
// skipSpace skips one or multiple spaces and tabs in b.
func (s *headerScanner) skipSpace() bool {
skipped := false
for {
c := s.b[s.r]
if c != ' ' && c != '\t' {
break
}
s.r++
skipped = true
}
return skipped
}
func isASCIILetter(b byte) bool {
b |= 0x20 // Make lower case.
return 'a' <= b && b <= 'z'
}
// trim returns s with leading and trailing spaces and tabs removed.
// It does not assume Unicode or UTF-8.
func trim(s []byte) []byte {
i := 0
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
n := len(s)
for n > i && (s[n-1] == ' ' || s[n-1] == '\t') {
n--
}
return s[i:n]
}
func trimTrailingSpace(s []byte) []byte {
for len(s) > 0 {
c := s[len(s)-1]
if c != ' ' && c != '\t' {
break
}
s = s[:len(s)-1]
}
return s
}