mirror of
https://github.com/pgsty/minio.git
synced 2026-10-06 17:55:59 +03:00
fix(http): enforce absolute read header timeout against slow HTTP DoS
Slow HTTP DoS scanners (issue #183) hold connections open by dribbling request bytes, and two inherited gaps let them: 1. deadlineconn re-extended the read deadline on every partial read, converting net/http's absolute ReadHeaderTimeout into an inter-byte activity timeout that a trickle bypasses entirely. 2. the --read-header-timeout flag (default 30s) was never copied into the server context, so the server actually ran with header timeout disabled (upstream shares this bug). Keep deadlineconn on the write side only so response writes stay activity-based, and leave ReadTimeout at zero now that native read deadlines are honored, since an absolute cap would terminate large uploads. Request bodies keep an activity-based bound instead via a per-read deadline refresh in xhttp, and the flag is wired through common-main. With this, a slow-header connection is cut at ReadHeaderTimeout (verified live: 33s kill vs. unlimited before), while slow progressing uploads (32s for 2 MiB), stalled bodies (30s cleanup), slow readers (~30s write cutoff) and plain S3 traffic are unaffected. Fixes #183 Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -114,6 +115,19 @@ func (srv *Server) Init(listenCtx context.Context, listenErrCallback func(listen
|
||||
atomic.AddInt32(&srv.requestCount, 1)
|
||||
defer atomic.AddInt32(&srv.requestCount, -1)
|
||||
|
||||
// Bound request bodies by read activity: refresh the connection
|
||||
// read deadline ahead of every body read so a stalled upload is
|
||||
// cut off while a slow but progressing one is never terminated.
|
||||
// This replaces the connection-level read idle timeout that had
|
||||
// to be dropped to keep ReadHeaderTimeout absolute (slowloris).
|
||||
if idle := srv.IdleTimeout; idle > 0 && r.Body != nil && r.Body != http.NoBody {
|
||||
r.Body = &idleTimeoutBody{
|
||||
rc: http.NewResponseController(w),
|
||||
body: r.Body,
|
||||
idle: idle,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle request using passed handler.
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
@@ -217,6 +231,28 @@ func (srv *Server) UseTCPOptions(opts TCPOptions) *Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
// idleTimeoutBody wraps a request body and refreshes the connection read
|
||||
// deadline ahead of every read, giving bodies an activity-based timeout:
|
||||
// reads that stall longer than idle fail with a timeout error while reads
|
||||
// that keep making progress extend the deadline indefinitely.
|
||||
type idleTimeoutBody struct {
|
||||
rc *http.ResponseController
|
||||
body io.ReadCloser
|
||||
idle time.Duration
|
||||
}
|
||||
|
||||
func (b *idleTimeoutBody) Read(p []byte) (n int, err error) {
|
||||
// Best effort: on connections where deadline control is unavailable
|
||||
// (e.g. hijacked) the call is a no-op and reads stay unbounded, as
|
||||
// they were before.
|
||||
_ = b.rc.SetReadDeadline(time.Now().Add(b.idle))
|
||||
return b.body.Read(p)
|
||||
}
|
||||
|
||||
func (b *idleTimeoutBody) Close() error {
|
||||
return b.body.Close()
|
||||
}
|
||||
|
||||
// NewServer - creates new HTTP server using given arguments.
|
||||
func NewServer(addrs []string) *Server {
|
||||
httpServer := &Server{
|
||||
|
||||
Reference in New Issue
Block a user