From ca803fd02a6a43f798ac3ccfea32351d4c9db0c9 Mon Sep 17 00:00:00 2001 From: chihskin-afk Date: Thu, 20 Aug 2026 10:28:25 +0300 Subject: [PATCH] feat(engine): add read header timeout (#4760) --- gin.go | 14 +++++++++++ gin_integration_test.go | 51 +++++++++++++++++++++++++++++++++++++++++ go.mod | 10 ++++---- go.sum | 23 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/gin.go b/gin.go index 2e033bf3..71bc83ec 100644 --- a/gin.go +++ b/gin.go @@ -13,6 +13,7 @@ import ( "path" "strings" "sync" + "time" "github.com/gin-gonic/gin/internal/bytesconv" filesystem "github.com/gin-gonic/gin/internal/fs" @@ -24,6 +25,7 @@ import ( const ( defaultMultipartMemory = 32 << 20 // 32 MB + defaultReadHeaderTimeout = 5 * time.Second escapedColon = "\\:" colon = ":" backslash = "\\" @@ -122,6 +124,16 @@ type Engine struct { // handler. HandleMethodNotAllowed bool + // ReadHeaderTimeout is the maximum duration for reading the entire + // request header, including the body. A zero or negative value means + // there will be no timeout. + // This setting helps protect against Slowloris attacks by limiting + // the time a client can take to send headers. If the timeout expires + // before the full header is received, the server closes the connection. + // It corresponds directly to http.Server.ReadHeaderTimeout in the + // standard library. + ReadHeaderTimeout time.Duration + // ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that // match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was // fetched, it falls back to the IP obtained from @@ -210,6 +222,7 @@ func New(opts ...OptionFunc) *Engine { FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, + ReadHeaderTimeout: defaultReadHeaderTimeout, HandleMethodNotAllowed: false, ForwardedByClientIP: true, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, @@ -550,6 +563,7 @@ func (engine *Engine) Run(addr ...string) (err error) { server := &http.Server{ // #nosec G112 Addr: address, Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.ListenAndServe() return diff --git a/gin_integration_test.go b/gin_integration_test.go index 720b140f..3a4e9651 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -605,3 +605,54 @@ func TestEscapedColon(t *testing.T) { testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r") testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r") } + +func TestEngineReadHeaderTimeout(t *testing.T) { + const timeout = 200 * time.Millisecond + + router := New() + router.ReadHeaderTimeout = timeout + router.GET("/test", func(c *Context) { + c.String(http.StatusOK, "ok") + }) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + go func() { + _ = http.Serve(ln, router.Handler()) + }() + + conn, err := net.Dial("tcp", ln.Addr().String()) + require.NoError(t, err) + defer conn.Close() + + _, err = conn.Write([]byte("GET /test HTTP/1.1\r\nHost: localhost\r\n")) + require.NoError(t, err) + + conn.SetReadDeadline(time.Now().Add(timeout + 500*time.Millisecond)) + + buf := make([]byte, 1024) + _, err = conn.Read(buf) + + assert.Error(t, err, "expected connection to be closed by server due to ReadHeaderTimeout") + + if err == nil { + t.Fatalf("expected error but got response: %s", string(buf)) + } +} + +func TestEngineReadHeaderTimeoutSuccess(t *testing.T) { + router := New() + router.ReadHeaderTimeout = 5 * time.Second + router.GET("/test", func(c *Context) { + c.String(http.StatusOK, "success") + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "success", w.Body.String()) +} diff --git a/go.mod b/go.mod index 3f80e1c0..c67158cc 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver/v2 v2.5.0 - golang.org/x/net v0.56.0 + golang.org/x/net v0.58.0 google.golang.org/protobuf v1.36.11 ) @@ -39,7 +39,9 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.25.0 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.49.0 // indirect ) diff --git a/go.sum b/go.sum index f04ab19c..07920a7c 100644 --- a/go.sum +++ b/go.sum @@ -79,15 +79,38 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA= +golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=