@@ -45,6 +45,10 @@ type ServerConfig struct {
4545 IdleTimeout time.Duration `toml:"idle_timeout"`
4646 // ShutdownTimeout is how long to wait for in-flight requests during shutdown.
4747 ShutdownTimeout time.Duration `toml:"shutdown_timeout"`
48+ // MaxConnsPerIP limits the number of concurrent connections from a single IP
49+ // address. 0 means unlimited. Default: 0 (disabled).
50+ // SEC-015: Rate-limiting defence against connection exhaustion attacks.
51+ MaxConnsPerIP int `toml:"max_conns_per_ip"`
4852}
4953
5054// FilesConfig holds file-serving settings.
@@ -55,6 +59,12 @@ type FilesConfig struct {
5559 Index string `toml:"index"`
5660 // NotFound is the path (relative to Root) of the custom 404 page.
5761 NotFound string `toml:"not_found"`
62+ // MaxServeFileSize is the maximum file size (in bytes) that will be served.
63+ // Files exceeding this limit receive a 413 Payload Too Large response.
64+ // This prevents a single request from loading an arbitrarily large file
65+ // into memory. Default: 1 GB. Set to 0 to disable the limit.
66+ // SEC-011: Hard upper bound for large file serving.
67+ MaxServeFileSize int64 `toml:"max_serve_file_size"`
5868}
5969
6070// CacheConfig holds in-memory cache settings.
@@ -89,6 +99,12 @@ type CompressionConfig struct {
8999 Level int `toml:"level"`
90100 // Precompressed enables serving pre-compressed .gz/.br sidecar files. Default: true.
91101 Precompressed bool `toml:"precompressed"`
102+ // MaxCompressSize is the maximum response body size (in bytes) eligible for
103+ // on-the-fly gzip compression. Bodies exceeding this limit are served
104+ // uncompressed to avoid excessive memory allocation and CPU usage.
105+ // Default: 10 MB. Set to 0 to disable the limit.
106+ // SEC-005: Bounds on-the-fly compression memory usage.
107+ MaxCompressSize int `toml:"max_compress_size"`
92108}
93109
94110// HeadersConfig controls HTTP response header settings.
@@ -154,6 +170,7 @@ func applyDefaults(cfg *Config) {
154170
155171 cfg .Files .Root = "./public"
156172 cfg .Files .Index = "index.html"
173+ cfg .Files .MaxServeFileSize = 1024 * 1024 * 1024 // 1 GB
157174
158175 cfg .Cache .Enabled = true
159176 cfg .Cache .MaxBytes = 256 * 1024 * 1024 // 256 MB
@@ -163,6 +180,7 @@ func applyDefaults(cfg *Config) {
163180 cfg .Compression .MinSize = 1024
164181 cfg .Compression .Level = 5
165182 cfg .Compression .Precompressed = true
183+ cfg .Compression .MaxCompressSize = 10 * 1024 * 1024 // 10 MB
166184
167185 cfg .Headers .StaticMaxAge = 3600
168186 cfg .Headers .HTMLMaxAge = 0
@@ -225,6 +243,11 @@ func applyEnvOverrides(cfg *Config) {
225243 if v := os .Getenv ("STATIC_FILES_NOT_FOUND" ); v != "" {
226244 cfg .Files .NotFound = v
227245 }
246+ if v := os .Getenv ("STATIC_FILES_MAX_SERVE_FILE_SIZE" ); v != "" {
247+ if n , err := strconv .ParseInt (v , 10 , 64 ); err == nil {
248+ cfg .Files .MaxServeFileSize = n
249+ }
250+ }
228251
229252 if v := os .Getenv ("STATIC_CACHE_ENABLED" ); v != "" {
230253 cfg .Cache .Enabled = strings .EqualFold (v , "true" ) || v == "1"
@@ -266,6 +289,11 @@ func applyEnvOverrides(cfg *Config) {
266289 cfg .Compression .Level = n
267290 }
268291 }
292+ if v := os .Getenv ("STATIC_COMPRESSION_MAX_COMPRESS_SIZE" ); v != "" {
293+ if n , err := strconv .Atoi (v ); err == nil {
294+ cfg .Compression .MaxCompressSize = n
295+ }
296+ }
269297
270298 if v := os .Getenv ("STATIC_SECURITY_BLOCK_DOTFILES" ); v != "" {
271299 cfg .Security .BlockDotfiles = strings .EqualFold (v , "true" ) || v == "1"
@@ -284,4 +312,9 @@ func applyEnvOverrides(cfg *Config) {
284312 if v := os .Getenv ("STATIC_HEADERS_ENABLE_ETAGS" ); v != "" {
285313 cfg .Headers .EnableETags = strings .EqualFold (v , "true" ) || v == "1"
286314 }
315+ if v := os .Getenv ("STATIC_SERVER_MAX_CONNS_PER_IP" ); v != "" {
316+ if n , err := strconv .Atoi (v ); err == nil {
317+ cfg .Server .MaxConnsPerIP = n
318+ }
319+ }
287320}
0 commit comments