diff --git a/.golangci.yml b/.golangci.yml index 19a2889130..dc8c148c5b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -65,6 +65,7 @@ linters-settings: revive: # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md rules: + - name: indent-error-flow - name: use-any lll: line-length: 130 diff --git a/client.go b/client.go index 84a7f93edb..7b88a7815a 100644 --- a/client.go +++ b/client.go @@ -3060,9 +3060,8 @@ func (t *transport) RoundTrip(hc *HostClient, req *Request, resp *Response) (ret return nil }) return false, nil - } else { - hc.releaseReader(br) } + hc.releaseReader(br) if closeConn { hc.closeConn(cc) diff --git a/cookie.go b/cookie.go index 3b1fe6b987..f4ea24c4cd 100644 --- a/cookie.go +++ b/cookie.go @@ -77,6 +77,9 @@ type Cookie struct { bufK []byte bufV []byte + // maxAge=0 means no 'max-age' attribute specified. + // maxAge<0 means delete cookie now, equivalently 'max-age=0' + // maxAge>0 means 'max-age' attribute present and given in seconds maxAge int sameSite CookieSameSite @@ -193,7 +196,10 @@ func (c *Cookie) MaxAge() int { // SetMaxAge sets cookie expiration time based on seconds. This takes precedence // over any absolute expiry set on the cookie. // -// Set max age to 0 to unset. +// 'max-age' is set when the maxAge is non-zero. That is, if maxAge = 0, +// the 'max-age' is unset. If maxAge < 0, it indicates that the cookie should +// be deleted immediately, equivalent to 'max-age=0'. This behavior is +// consistent with the Go standard library's net/http package. func (c *Cookie) SetMaxAge(seconds int) { c.maxAge = seconds } @@ -278,11 +284,16 @@ func (c *Cookie) AppendBytes(dst []byte) []byte { } dst = append(dst, c.value...) - if c.maxAge > 0 { + if c.maxAge != 0 { dst = append(dst, ';', ' ') dst = append(dst, strCookieMaxAge...) dst = append(dst, '=') - dst = AppendUint(dst, c.maxAge) + if c.maxAge < 0 { + // See https://github.com/valyala/fasthttp/issues/1900 + dst = AppendUint(dst, 0) + } else { + dst = AppendUint(dst, c.maxAge) + } } else if !c.expire.IsZero() { c.bufV = AppendHTTPDate(c.bufV[:0], c.expire) dst = append(dst, ';', ' ') diff --git a/cookie_test.go b/cookie_test.go index 4a87f155e2..2bc4d64b02 100644 --- a/cookie_test.go +++ b/cookie_test.go @@ -213,6 +213,13 @@ func TestCookieMaxAge(t *testing.T) { if s != "foo=bar; expires=Thu, 01 Jan 1970 00:01:40 GMT" { t.Fatalf("missing expires %q", s) } + + c.SetMaxAge(-100) + result := strings.ToLower(c.String()) + const expectedMaxAge0 = "max-age=0" + if !strings.Contains(result, expectedMaxAge0) { + t.Fatalf("Unexpected cookie %q. Should contain %q", result, expectedMaxAge0) + } } func TestCookieHttpOnly(t *testing.T) { diff --git a/go.mod b/go.mod index dc8115bd37..6e0d73474a 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/tcplisten v1.0.0 golang.org/x/crypto v0.29.0 - golang.org/x/net v0.30.0 + golang.org/x/net v0.31.0 golang.org/x/sys v0.27.0 ) diff --git a/go.sum b/go.sum index 147d46376b..2f722a99f3 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= diff --git a/http.go b/http.go index 17d6b7dbe8..99a8bddf8a 100644 --- a/http.go +++ b/http.go @@ -1590,9 +1590,8 @@ func (req *Request) Write(w *bufio.Writer) error { if len(req.Header.Host()) == 0 { if len(host) == 0 { return errRequestHostRequired - } else { - req.Header.SetHostBytes(host) } + req.Header.SetHostBytes(host) } else if !req.UseHostHeader { req.Header.SetHostBytes(host) } @@ -2506,17 +2505,24 @@ func parseChunkSize(r *bufio.Reader) (int, error) { c, err := r.ReadByte() if err != nil { return -1, ErrBrokenChunk{ - error: fmt.Errorf("cannot read '\r' char at the end of chunk size: %w", err), + error: fmt.Errorf("cannot read '\\r' char at the end of chunk size: %w", err), } } // Skip chunk extension after chunk size. // Add support later if anyone needs it. if c != '\r' { + // Security: Don't allow newlines in chunk extensions. + // This can lead to request smuggling issues with some reverse proxies. + if c == '\n' { + return -1, ErrBrokenChunk{ + error: errors.New("invalid character '\\n' after chunk size"), + } + } continue } if err := r.UnreadByte(); err != nil { return -1, ErrBrokenChunk{ - error: fmt.Errorf("cannot unread '\r' char at the end of chunk size: %w", err), + error: fmt.Errorf("cannot unread '\\r' char at the end of chunk size: %w", err), } } break diff --git a/uri.go b/uri.go index cca09517a2..11f7b03bda 100644 --- a/uri.go +++ b/uri.go @@ -312,11 +312,11 @@ func (u *URI) parse(host, uri []byte, isTLS bool) error { } u.host = append(u.host, host...) - if parsedHost, err := parseHost(u.host); err != nil { + parsedHost, err := parseHost(u.host) + if err != nil { return err - } else { - u.host = parsedHost } + u.host = parsedHost lowercaseBytes(u.host) b := uri