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
|
||||
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
|
||||
// directly to the public internet, so a single host must not be able to
|
||||
// exhaust the global connection budget or flood new-connection attempts.
|
||||
@@ -43,6 +52,7 @@ func DefaultConfig() Config {
|
||||
KeepAliveTime: time.Minute * 5,
|
||||
ReadTimeout: time.Minute * 2,
|
||||
WriteTimeout: time.Second * 30,
|
||||
MaxBufferedBytes: 50*1024*1024 + 64*1024,
|
||||
PerIPMaxConnections: 256,
|
||||
PerIPConnRateLimit: 120,
|
||||
PerIPConnRateWindow: time.Minute,
|
||||
@@ -399,6 +409,15 @@ func (s *Server) handleConnection(conn net.Conn) {
|
||||
// Write to input buffer
|
||||
_, _ = 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
|
||||
s.processData(ctx)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
"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