Harden HTTP defaults and proxy header trust.
Add explicit server timeout/header limits, require trusted proxy CIDRs when honoring forwarded IP headers, and document single-instance storage expectations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,11 @@ import (
|
||||
|
||||
const (
|
||||
defaultListenAddr = ":8080"
|
||||
defaultReadHeaderTimeout = "5s"
|
||||
defaultReadTimeout = "30s"
|
||||
defaultWriteTimeout = "30s"
|
||||
defaultIdleTimeout = "120s"
|
||||
defaultMaxHeaderBytes = 1 << 20
|
||||
defaultMaxUploadSize = "100MiB"
|
||||
defaultTTL = "1h"
|
||||
defaultRawCacheMaxSize = "200MiB"
|
||||
@@ -36,7 +41,16 @@ type Config struct {
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
ListenAddr string `yaml:"listen_addr" comment:"Address to bind the HTTP server to."`
|
||||
ListenAddr string `yaml:"listen_addr" comment:"Address to bind the HTTP server to."`
|
||||
ReadHeaderTimeout string `yaml:"read_header_timeout" comment:"Maximum duration for reading request headers (for slowloris protection). Must be > 0."`
|
||||
ReadTimeout string `yaml:"read_timeout" comment:"Maximum duration for reading the full request, including body. Must be > 0."`
|
||||
WriteTimeout string `yaml:"write_timeout" comment:"Maximum duration before timing out writes of a response. Must be > 0."`
|
||||
IdleTimeout string `yaml:"idle_timeout" comment:"Maximum amount of time to wait for the next request when keep-alives are enabled. Must be > 0."`
|
||||
MaxHeaderBytes int `yaml:"max_header_bytes" comment:"Maximum size of request headers in bytes. Must be > 0."`
|
||||
ReadHeaderTimeoutDur time.Duration `yaml:"-"`
|
||||
ReadTimeoutDur time.Duration `yaml:"-"`
|
||||
WriteTimeoutDur time.Duration `yaml:"-"`
|
||||
IdleTimeoutDur time.Duration `yaml:"-"`
|
||||
}
|
||||
|
||||
type LimitsConfig struct {
|
||||
@@ -56,10 +70,12 @@ type StorageConfig struct {
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
AllowedIPs []string `yaml:"allowed_ips" comment:"Allowlist for POST /api/scratch as IPs or CIDRs (examples: 127.0.0.1, 192.168.1.0/24). Keep 127.0.0.1 for local access; add trusted ranges as needed. Empty means unrestricted."`
|
||||
TrustProxyHeaders bool `yaml:"trust_proxy_headers" comment:"Honor X-Forwarded-For / X-Real-IP when true (trusted proxy only)."`
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit" comment:"Per-client token bucket on POST /api/scratch."`
|
||||
AllowedPrefixes []netip.Prefix `yaml:"-"`
|
||||
AllowedIPs []string `yaml:"allowed_ips" comment:"Allowlist for POST /api/scratch as IPs or CIDRs (examples: 127.0.0.1, 192.168.1.0/24). Keep 127.0.0.1 for local access; add trusted ranges as needed. Empty means unrestricted."`
|
||||
TrustProxyHeaders bool `yaml:"trust_proxy_headers" comment:"Honor X-Forwarded-For / X-Real-IP only when request source IP matches security.trusted_proxy_ips."`
|
||||
TrustedProxyIPs []string `yaml:"trusted_proxy_ips" comment:"IPs/CIDRs of reverse proxies allowed to supply forwarded client IP headers. Required when trust_proxy_headers is true."`
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit" comment:"Per-client token bucket on POST /api/scratch."`
|
||||
AllowedPrefixes []netip.Prefix `yaml:"-"`
|
||||
TrustedProxyPrefixes []netip.Prefix `yaml:"-"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
@@ -93,7 +109,12 @@ func Default() Config {
|
||||
func defaults() Config {
|
||||
return Config{
|
||||
Server: ServerConfig{
|
||||
ListenAddr: defaultListenAddr,
|
||||
ListenAddr: defaultListenAddr,
|
||||
ReadHeaderTimeout: defaultReadHeaderTimeout,
|
||||
ReadTimeout: defaultReadTimeout,
|
||||
WriteTimeout: defaultWriteTimeout,
|
||||
IdleTimeout: defaultIdleTimeout,
|
||||
MaxHeaderBytes: defaultMaxHeaderBytes,
|
||||
},
|
||||
Limits: LimitsConfig{
|
||||
MaxUploadSize: defaultMaxUploadSize,
|
||||
@@ -122,6 +143,45 @@ func (c *Config) Validate() error {
|
||||
if strings.TrimSpace(c.Server.ListenAddr) == "" {
|
||||
return errors.New("server.listen_addr is required")
|
||||
}
|
||||
readHeaderTimeout, err := time.ParseDuration(strings.TrimSpace(c.Server.ReadHeaderTimeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("server.read_header_timeout invalid: %w", err)
|
||||
}
|
||||
if readHeaderTimeout <= 0 {
|
||||
return errors.New("server.read_header_timeout must be > 0")
|
||||
}
|
||||
c.Server.ReadHeaderTimeoutDur = readHeaderTimeout
|
||||
|
||||
readTimeout, err := time.ParseDuration(strings.TrimSpace(c.Server.ReadTimeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("server.read_timeout invalid: %w", err)
|
||||
}
|
||||
if readTimeout <= 0 {
|
||||
return errors.New("server.read_timeout must be > 0")
|
||||
}
|
||||
c.Server.ReadTimeoutDur = readTimeout
|
||||
|
||||
writeTimeout, err := time.ParseDuration(strings.TrimSpace(c.Server.WriteTimeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("server.write_timeout invalid: %w", err)
|
||||
}
|
||||
if writeTimeout <= 0 {
|
||||
return errors.New("server.write_timeout must be > 0")
|
||||
}
|
||||
c.Server.WriteTimeoutDur = writeTimeout
|
||||
|
||||
idleTimeout, err := time.ParseDuration(strings.TrimSpace(c.Server.IdleTimeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("server.idle_timeout invalid: %w", err)
|
||||
}
|
||||
if idleTimeout <= 0 {
|
||||
return errors.New("server.idle_timeout must be > 0")
|
||||
}
|
||||
c.Server.IdleTimeoutDur = idleTimeout
|
||||
|
||||
if c.Server.MaxHeaderBytes <= 0 {
|
||||
return errors.New("server.max_header_bytes must be > 0")
|
||||
}
|
||||
|
||||
sizeBytes, err := parseHumanSize(c.Limits.MaxUploadSize)
|
||||
if err != nil {
|
||||
@@ -168,6 +228,14 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("security.allowed_ips invalid: %w", err)
|
||||
}
|
||||
c.Security.AllowedPrefixes = prefixes
|
||||
trustedProxyPrefixes, err := parseAllowedPrefixes(c.Security.TrustedProxyIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("security.trusted_proxy_ips invalid: %w", err)
|
||||
}
|
||||
c.Security.TrustedProxyPrefixes = trustedProxyPrefixes
|
||||
if c.Security.TrustProxyHeaders && len(c.Security.TrustedProxyPrefixes) == 0 {
|
||||
return errors.New("security.trusted_proxy_ips is required when security.trust_proxy_headers is true")
|
||||
}
|
||||
|
||||
if c.Security.RateLimit.RequestsPerMinute <= 0 {
|
||||
return errors.New("security.rate_limit.requests_per_minute must be > 0")
|
||||
|
||||
Reference in New Issue
Block a user