Security: Cap per-connection inbound buffer to prevent memory DoS
The protocol length field is validated to 50 MiB, but a peer that sends bytes which never assemble into a complete packet — for example an HTTP-mask envelope without its terminating CRLF CRLF, which Parse() waits on indefinitely — could grow the per-connection input buffer without bound. handleConnection now closes the connection once unparsed buffered bytes exceed MaxBufferedBytes (default 50 MiB + 64 KiB slack, covering the largest legitimate packet plus its mask envelope and a read chunk). Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
8e0f1283c6
commit
870a5832ea
2 files changed
+58
No files matched your search
@@ -23,6 +23,15 @@ type Config struct {
|
|||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
|
|
||||||
|
// MaxBufferedBytes is a hard cap on unparsed inbound bytes buffered per
|
||||||
|
// connection. The protocol's own length field is validated to 50 MiB, but
|
||||||
|
// a peer that sends bytes that never assemble into a complete packet (e.g.
|
||||||
|
// an HTTP-mask envelope without its terminating CRLF CRLF, which Parse()
|
||||||
|
// waits on indefinitely) could otherwise grow the buffer without bound.
|
||||||
|
// Default leaves slack for the largest legitimate packet plus its mask
|
||||||
|
// envelope and a read chunk.
|
||||||
|
MaxBufferedBytes int
|
||||||
|
|
||||||
// Per-IP connection limits. The TCP listener (port 6543) is exposed
|
// Per-IP connection limits. The TCP listener (port 6543) is exposed
|
||||||
// directly to the public internet, so a single host must not be able to
|
// directly to the public internet, so a single host must not be able to
|
||||||
// exhaust the global connection budget or flood new-connection attempts.
|
// exhaust the global connection budget or flood new-connection attempts.
|
||||||
@@ -43,6 +52,7 @@ func DefaultConfig() Config {
|
|||||||
KeepAliveTime: time.Minute * 5,
|
KeepAliveTime: time.Minute * 5,
|
||||||
ReadTimeout: time.Minute * 2,
|
ReadTimeout: time.Minute * 2,
|
||||||
WriteTimeout: time.Second * 30,
|
WriteTimeout: time.Second * 30,
|
||||||
|
MaxBufferedBytes: 50*1024*1024 + 64*1024,
|
||||||
PerIPMaxConnections: 256,
|
PerIPMaxConnections: 256,
|
||||||
PerIPConnRateLimit: 120,
|
PerIPConnRateLimit: 120,
|
||||||
PerIPConnRateWindow: time.Minute,
|
PerIPConnRateWindow: time.Minute,
|
||||||
@@ -399,6 +409,15 @@ func (s *Server) handleConnection(conn net.Conn) {
|
|||||||
// Write to input buffer
|
// Write to input buffer
|
||||||
_, _ = ctx.InBuffer.Write(buf[:n])
|
_, _ = ctx.InBuffer.Write(buf[:n])
|
||||||
|
|
||||||
|
// Hard cap on unparsed inbound bytes (memory-DoS guard). A peer
|
||||||
|
// that sends bytes that never assemble into a complete packet can
|
||||||
|
// otherwise grow this buffer without bound.
|
||||||
|
if ctx.InBuffer.Len() > s.config.MaxBufferedBytes {
|
||||||
|
s.logger.Warn("Inbound buffer exceeded %d bytes from ip=%s conn=%d — closing",
|
||||||
|
s.config.MaxBufferedBytes, ctx.GetPeerIP(), ctx.ID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// Process received data
|
// Process received data
|
||||||
s.processData(ctx)
|
s.processData(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -91,3 +92,41 @@ func TestPeerIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A peer that sends bytes which never assemble into a complete packet (here an
|
||||||
|
// HTTP-mask envelope with no terminating CRLF CRLF) must be disconnected once
|
||||||
|
// the per-connection buffer cap is exceeded, rather than allowed to grow it
|
||||||
|
// without bound.
|
||||||
|
func TestHandleConnectionBufferCap(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.MaxBufferedBytes = 1024 // small cap for a fast, in-memory test
|
||||||
|
cfg.ReadTimeout = time.Second
|
||||||
|
|
||||||
|
srv := New(cfg)
|
||||||
|
srv.running.Store(true) // bypass acceptLoop; exercise handleConnection directly
|
||||||
|
|
||||||
|
client, serverConn := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
srv.handleConnection(serverConn)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// "POST " prefix with no terminator, plus filler to exceed the cap.
|
||||||
|
payload := append([]byte("POST /x HTTP/1.1\r\n"), bytes.Repeat([]byte{'A'}, 4096)...)
|
||||||
|
writeErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := client.Write(payload)
|
||||||
|
writeErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// handleConnection returned because the cap was exceeded and it closed
|
||||||
|
// the connection.
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("connection was not closed after exceeding the buffer cap")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user