diff --git a/docs/doc.md b/docs/doc.md index d1c33b87..65ede078 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -2091,6 +2091,25 @@ See a complete example in the `https://github.com/gin-gonic/examples/tree/master > Configure HTTP servers, TLS, proxies, and runtime settings. +### Default ReadHeaderTimeout + +`Run`, `RunTLS`, `RunUnix`, and `RunListener` build their `http.Server` with a default +`ReadHeaderTimeout` of 5 seconds (`engine.ReadHeaderTimeout`), which mitigates +Slowloris-style attacks where a client trickles request headers in slowly to exhaust +server connections. It only bounds the time to read headers, not the request body or +the response, so it's safe for streaming/SSE/long-polling handlers. + +To use a different value, or to disable it and restore the previous unbounded +behavior, set the field before calling `Run`: + +```go +func main() { + router := gin.Default() + router.ReadHeaderTimeout = 0 // disable, or set your own duration + router.Run(":8080") +} +``` + ### Custom HTTP configuration Use `http.ListenAndServe()` directly, like this: diff --git a/gin.go b/gin.go index 2e033bf3..b7baa4c2 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" @@ -23,10 +24,11 @@ import ( ) const ( - defaultMultipartMemory = 32 << 20 // 32 MB - escapedColon = "\\:" - colon = ":" - backslash = "\\" + defaultMultipartMemory = 32 << 20 // 32 MB + defaultReadHeaderTimeout = 5 * time.Second + escapedColon = "\\:" + colon = ":" + backslash = "\\" ) var ( @@ -166,6 +168,14 @@ type Engine struct { // method call. MaxMultipartMemory int64 + // ReadHeaderTimeout is the amount of time allowed to read request headers on + // connections created by Run, RunTLS, RunUnix, and RunListener. It protects + // against Slowloris-style attacks where a client trickles headers in slowly. + // It does not bound how long handlers may take to read the body or write the + // response, so it is safe for streaming/SSE/long-polling handlers. + // Set to 0 to disable (restores pre-default unbounded behavior). + ReadHeaderTimeout time.Duration + // UseH2C enable h2c support. UseH2C bool @@ -199,6 +209,7 @@ var _ IRouter = (*Engine)(nil) // - UseRawPath: false // - UseEscapedPath: false // - UnescapePathValues: true +// - ReadHeaderTimeout: 5 * time.Second func New(opts ...OptionFunc) *Engine { debugPrintWARNINGNew() engine := &Engine{ @@ -219,6 +230,7 @@ func New(opts ...OptionFunc) *Engine { RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, + ReadHeaderTimeout: defaultReadHeaderTimeout, trees: make(methodTrees, 0, 9), delims: render.Delims{Left: "{{", Right: "}}"}, secureJSONPrefix: "while(1);", @@ -547,9 +559,10 @@ func (engine *Engine) Run(addr ...string) (err error) { engine.updateRouteTrees() address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) - server := &http.Server{ // #nosec G112 - Addr: address, - Handler: engine.Handler(), + server := &http.Server{ + Addr: address, + Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.ListenAndServe() return @@ -567,9 +580,10 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) { "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") } - server := &http.Server{ // #nosec G112 - Addr: addr, - Handler: engine.Handler(), + server := &http.Server{ + Addr: addr, + Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.ListenAndServeTLS(certFile, keyFile) return @@ -594,8 +608,9 @@ func (engine *Engine) RunUnix(file string) (err error) { defer listener.Close() defer os.Remove(file) - server := &http.Server{ // #nosec G112 - Handler: engine.Handler(), + server := &http.Server{ + Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.Serve(listener) return @@ -651,8 +666,9 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) { "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") } - server := &http.Server{ // #nosec G112 - Handler: engine.Handler(), + server := &http.Server{ + Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.Serve(listener) return diff --git a/gin_integration_test.go b/gin_integration_test.go index 720b140f..83f55a6b 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -245,6 +245,37 @@ func TestRunWithPort(t *testing.T) { testRequest(t, "http://localhost:5150/example") } +func TestRunDefaultReadHeaderTimeout(t *testing.T) { + router := New() + router.ReadHeaderTimeout = 200 * time.Millisecond + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + go func() { + assert.NoError(t, router.Run(":5151")) + }() + + // Wait for server to be ready with exponential backoff + err := waitForServerReady("http://localhost:5151/example", 10) + require.NoError(t, err, "server should start successfully") + + // A normal request should still succeed unaffected by the header timeout. + testRequest(t, "http://localhost:5151/example") + + // Open a raw connection and trickle in a partial request header, + // never completing it. The server should close the connection once + // ReadHeaderTimeout elapses instead of waiting forever. + conn, err := net.Dial("tcp", "localhost:5151") + require.NoError(t, err) + defer conn.Close() + + _, err = fmt.Fprint(conn, "GET /example HTTP/1.1\r\nHost: localhost\r\n") + require.NoError(t, err) + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second))) + buf := make([]byte, 1) + _, err = conn.Read(buf) + assert.ErrorIs(t, err, io.EOF, "connection should be closed by the server after ReadHeaderTimeout") +} + func TestUnixSocket(t *testing.T) { router := New() diff --git a/gin_test.go b/gin_test.go index a9cf1755..54dc3390 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1024,6 +1024,22 @@ func TestWithOptionFunc(t *testing.T) { assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"}) } +func TestNewDefaultReadHeaderTimeout(t *testing.T) { + r := New() + assert.Equal(t, defaultReadHeaderTimeout, r.ReadHeaderTimeout) +} + +func TestNewReadHeaderTimeoutOverride(t *testing.T) { + r := New(func(e *Engine) { + e.ReadHeaderTimeout = 0 + }) + assert.Equal(t, time.Duration(0), r.ReadHeaderTimeout) + + r2 := New() + r2.ReadHeaderTimeout = 30 * time.Second + assert.Equal(t, 30*time.Second, r2.ReadHeaderTimeout) +} + type Birthday string func (b *Birthday) UnmarshalParam(param string) error {