Skip to content

Commit 6c1948d

Browse files
committed
docs: update all documentation for security audit remediations
- Landing page (docs/index.html): add 3 new config fields to tables, update security tabs (DoS, TLS, runtime), architecture pipeline descriptions, and feature cards - README.md: update architecture diagram, config tables (+3 fields), env vars (+3 vars), DoS mitigations, and path-safety cache design - USER_GUIDE.md: update config example, env vars table, preload section (symlink validation, bounded LRU), add 413 troubleshooting entry - config.toml.example: add max_compress_size to [compression] section - CHANGELOG.md: add v1.6.2 entry covering all 16 security fixes, dependency bumps, and documentation updates
1 parent d26183c commit 6c1948d

File tree

5 files changed

+107
-20
lines changed

5 files changed

+107
-20
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
## v1.6.2 (2026-04-11)
2+
3+
### Security
4+
5+
- **SEC-001**: replace unbounded `sync.Map` path cache with bounded LRU (10k entries) to prevent memory exhaustion
6+
- **SEC-003**: suppress stack traces in recovery middleware unless `STATIC_DEBUG=1` is set
7+
- **SEC-004**: use `crypto/rand` for multipart range boundaries instead of hardcoded string
8+
- **SEC-005**: add `max_compress_size` config (default 10 MB) to cap on-the-fly compression
9+
- **SEC-006**: normalize cache keys with `path.Clean` to prevent cache poisoning via path variants
10+
- **SEC-007**: suppress server banner (`Server` header) on all responses
11+
- **SEC-008**: sanitize log output by escaping ASCII control characters in request URIs
12+
- **SEC-009**: remove deprecated `PreferServerCipherSuites` (Go runtime manages cipher order)
13+
- **SEC-010**: return 500 on directory listing template render failure instead of silently ignoring
14+
- **SEC-011**: add `max_serve_file_size` config (default 1 GB) with 413 response for oversized files
15+
- **SEC-014**: set `MaxRequestBodySize` to 1024 bytes (static file server needs no large uploads)
16+
- **SEC-015**: add `max_conns_per_ip` config for per-IP connection rate limiting
17+
- **SEC-016**: validate symlink targets stay within document root during preload
18+
19+
### Fix
20+
21+
- **deps**: bump andybalholm/brotli v1.2.0→v1.2.1, klauspost/compress v1.18.4→v1.18.5, valyala/fasthttp v1.69.0→v1.70.0
22+
23+
### Docs
24+
25+
- update landing page, README, USER_GUIDE, and config.toml.example with new config fields and security notes
26+
127
## v1.6.1 (2026-03-28)
228

329
### Fix

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ HTTP request
9191
│ • Method whitelist (GET/HEAD/OPTIONS only) │
9292
│ • Security headers (set BEFORE path check) │
9393
│ • PathSafe: null bytes, path.Clean, EvalSymlinks│
94-
│ • Path-safety cache (sync.Map, pre-warmed)
94+
│ • Path-safety cache (bounded LRU, pre-warmed) │
9595
│ • Dotfile blocking │
9696
│ • CORS (preflight + per-origin or wildcard *) │
9797
│ • Injects validated path into ctx.SetUserValue │
@@ -129,7 +129,7 @@ GET /app.js
129129
NO → os.Stat → os.ReadFile → cache.Put → serveFromCache
130130
```
131131

132-
When `preload = true`, every eligible file is loaded into cache at startup. The path-safety cache (`sync.Map`) is also pre-warmed, so the very first request for any preloaded file skips both filesystem I/O and `EvalSymlinks`.
132+
When `preload = true`, every eligible file is loaded into cache at startup. The path-safety cache (bounded LRU) is also pre-warmed, so the very first request for any preloaded file skips both filesystem I/O and `EvalSymlinks`. Symlink targets are validated against the root during the preload walk — symlinks pointing outside root are skipped.
133133

134134
---
135135

@@ -164,7 +164,7 @@ Measured on Apple M2 Pro (`go test -bench=. -benchtime=5s`):
164164
- **Direct `ctx.SetBody()` fast path**: cache hits bypass range/conditional logic entirely; pre-formatted `Content-Type` and `Content-Length` headers are assigned directly.
165165
- **Custom Range implementation**: `parseRange()`/`serveRange()` handle byte-range requests without `http.ServeContent`.
166166
- **Post-processing compression**: compress middleware runs after the handler, compressing the response body in a single pass.
167-
- **Path-safety cache**: `sync.Map`-based cache eliminates per-request `filepath.EvalSymlinks` syscalls. Pre-warmed from preload.
167+
- **Path-safety cache**: Bounded LRU cache (default 10,000 entries) eliminates per-request `filepath.EvalSymlinks` syscalls. Pre-warmed from preload.
168168
- **GC tuning**: `gc_percent = 400` reduces garbage collection frequency — the hot path avoids all formatting allocations, with only minimal byte-to-string conversions from fasthttp's `[]byte` API.
169169
- **Cache-before-stat**: `os.Stat` is never called on a cache hit — the hot path is pure memory.
170170
- **Zero-alloc `AcceptsEncoding`**: walks the `Accept-Encoding` header byte-by-byte without `strings.Split`.
@@ -214,7 +214,8 @@ Only `GET`, `HEAD`, and `OPTIONS` are accepted. All other methods (including `TR
214214
| `ReadTimeout` | 10 s (covers full read phase including headers — Slowloris protection) |
215215
| `WriteTimeout` | 10 s |
216216
| `IdleTimeout` | 75 s (keep-alive) |
217-
| `MaxRequestBodySize` | 0 (no body accepted — static server) |
217+
| `MaxRequestBodySize` | 1024 bytes (static file server needs no large request bodies) |
218+
| `MaxConnsPerIP` | Configurable (default 0 = unlimited) |
218219

219220
---
220221

@@ -235,6 +236,7 @@ Copy `config.toml.example` to `config.toml` and edit as needed. The server start
235236
| `write_timeout` | duration | `10s` | Response write deadline |
236237
| `idle_timeout` | duration | `75s` | Keep-alive idle timeout |
237238
| `shutdown_timeout` | duration | `15s` | Graceful drain window |
239+
| `max_conns_per_ip` | int | `0` | Max concurrent connections per IP (0 = unlimited) |
238240

239241
### `[files]`
240242

@@ -243,6 +245,7 @@ Copy `config.toml.example` to `config.toml` and edit as needed. The server start
243245
| `root` | string | `./public` | Directory to serve |
244246
| `index` | string | `index.html` | Index file for directory requests |
245247
| `not_found` | string || Custom 404 page (relative to `root`) |
248+
| `max_serve_file_size` | int | `1073741824` | Max file size to serve in bytes (0 = unlimited; default 1 GB). Files exceeding this limit receive 413. |
246249

247250
### `[cache]`
248251

@@ -263,6 +266,7 @@ Copy `config.toml.example` to `config.toml` and edit as needed. The server start
263266
| `min_size` | int | `1024` | Minimum bytes to compress |
264267
| `level` | int | `5` | gzip level (1–9) |
265268
| `precompressed` | bool | `true` | Serve `.gz`/`.br`/`.zst` sidecar files |
269+
| `max_compress_size` | int | `10485760` | Max body size for on-the-fly gzip compression in bytes (0 = unlimited; default 10 MB) |
266270

267271
### `[headers]`
268272

@@ -303,9 +307,11 @@ All environment variables override the corresponding TOML setting. Useful for co
303307
| `STATIC_SERVER_WRITE_TIMEOUT` | `server.write_timeout` |
304308
| `STATIC_SERVER_IDLE_TIMEOUT` | `server.idle_timeout` |
305309
| `STATIC_SERVER_SHUTDOWN_TIMEOUT` | `server.shutdown_timeout` |
310+
| `STATIC_SERVER_MAX_CONNS_PER_IP` | `server.max_conns_per_ip` |
306311
| `STATIC_FILES_ROOT` | `files.root` |
307312
| `STATIC_FILES_INDEX` | `files.index` |
308313
| `STATIC_FILES_NOT_FOUND` | `files.not_found` |
314+
| `STATIC_FILES_MAX_SERVE_FILE_SIZE` | `files.max_serve_file_size` |
309315
| `STATIC_CACHE_ENABLED` | `cache.enabled` |
310316
| `STATIC_CACHE_PRELOAD` | `cache.preload` |
311317
| `STATIC_CACHE_MAX_BYTES` | `cache.max_bytes` |
@@ -315,6 +321,7 @@ All environment variables override the corresponding TOML setting. Useful for co
315321
| `STATIC_COMPRESSION_ENABLED` | `compression.enabled` |
316322
| `STATIC_COMPRESSION_MIN_SIZE` | `compression.min_size` |
317323
| `STATIC_COMPRESSION_LEVEL` | `compression.level` |
324+
| `STATIC_COMPRESSION_MAX_COMPRESS_SIZE` | `compression.max_compress_size` |
318325
| `STATIC_HEADERS_ENABLE_ETAGS` | `headers.enable_etags` |
319326
| `STATIC_SECURITY_BLOCK_DOTFILES` | `security.block_dotfiles` |
320327
| `STATIC_SECURITY_CSP` | `security.csp` |

USER_GUIDE.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,13 @@ read_timeout = "10s" # full read deadline (covers headers; Slowloris
117117
write_timeout = "10s"
118118
idle_timeout = "75s"
119119
shutdown_timeout = "15s" # graceful drain window on SIGTERM/SIGINT
120+
# max_conns_per_ip = 0 # max concurrent connections per IP (0 = unlimited)
120121

121122
[files]
122123
root = "./public" # directory to serve
123124
index = "index.html" # index file for directory requests (e.g. GET /)
124125
not_found = "404.html" # custom 404 page, relative to root (optional)
126+
# max_serve_file_size = 1073741824 # files > 1 GB get 413 (0 = no limit)
125127

126128
[cache]
127129
enabled = true
@@ -136,6 +138,7 @@ enabled = true
136138
min_size = 1024 # don't compress responses smaller than 1 KB
137139
level = 5 # gzip level 1 (fastest) – 9 (best)
138140
precompressed = true # serve .gz / .br / .zst sidecar files when available
141+
# max_compress_size = 10485760 # skip on-the-fly gzip for bodies > 10 MB (0 = no limit)
139142

140143
[headers]
141144
immutable_pattern = "" # glob for fingerprinted assets → Cache-Control: immutable
@@ -169,9 +172,11 @@ Every config field can also be set via an environment variable, which takes prec
169172
| `STATIC_SERVER_WRITE_TIMEOUT` | `server.write_timeout` |
170173
| `STATIC_SERVER_IDLE_TIMEOUT` | `server.idle_timeout` |
171174
| `STATIC_SERVER_SHUTDOWN_TIMEOUT` | `server.shutdown_timeout` |
175+
| `STATIC_SERVER_MAX_CONNS_PER_IP` | `server.max_conns_per_ip` |
172176
| `STATIC_FILES_ROOT` | `files.root` |
173177
| `STATIC_FILES_INDEX` | `files.index` |
174178
| `STATIC_FILES_NOT_FOUND` | `files.not_found` |
179+
| `STATIC_FILES_MAX_SERVE_FILE_SIZE` | `files.max_serve_file_size` |
175180
| `STATIC_CACHE_ENABLED` | `cache.enabled` |
176181
| `STATIC_CACHE_PRELOAD` | `cache.preload` |
177182
| `STATIC_CACHE_MAX_BYTES` | `cache.max_bytes` |
@@ -181,6 +186,7 @@ Every config field can also be set via an environment variable, which takes prec
181186
| `STATIC_COMPRESSION_ENABLED` | `compression.enabled` |
182187
| `STATIC_COMPRESSION_MIN_SIZE` | `compression.min_size` |
183188
| `STATIC_COMPRESSION_LEVEL` | `compression.level` |
189+
| `STATIC_COMPRESSION_MAX_COMPRESS_SIZE` | `compression.max_compress_size` |
184190
| `STATIC_HEADERS_ENABLE_ETAGS` | `headers.enable_etags` |
185191
| `STATIC_SECURITY_BLOCK_DOTFILES` | `security.block_dotfiles` |
186192
| `STATIC_SECURITY_CSP` | `security.csp` |
@@ -644,10 +650,11 @@ STATIC_CACHE_PRELOAD=true STATIC_CACHE_GC_PERCENT=400 ./bin/static-web
644650
### What preloading does
645651
646652
1. At startup, walks every file under `files.root`.
647-
2. Files smaller than `max_file_size` are read into the LRU cache.
648-
3. Pre-formatted `Content-Type` and `Content-Length` response headers are computed once per file.
649-
4. The path-safety cache (`sync.Map`) is pre-warmed — the first request for any preloaded file skips `filepath.EvalSymlinks`.
650-
5. Preload statistics (file count, total bytes, duration) are logged at startup.
653+
2. Symlink targets are validated — symlinks pointing outside root are skipped.
654+
3. Files smaller than `max_file_size` are read into the LRU cache.
655+
4. Pre-formatted `Content-Type` and `Content-Length` response headers are computed once per file.
656+
5. The path-safety cache (bounded LRU) is pre-warmed — the first request for any preloaded file skips `filepath.EvalSymlinks`.
657+
6. Preload statistics (file count, total bytes, duration) are logged at startup.
651658
652659
### When to use preload
653660
@@ -783,6 +790,17 @@ The most common causes:
783790
784791
The server only accepts `GET`, `HEAD`, and `OPTIONS`. Any other method (POST, PUT, DELETE, PATCH, TRACE, etc.) is rejected with `405`. This is intentional — it's a static file server, not an API. If your browser is sending a `POST` request, check your HTML form actions and JavaScript fetch calls.
785792
793+
### `413 Payload Too Large`
794+
795+
A file exceeds the `max_serve_file_size` limit (default 1 GB). Increase the limit in config:
796+
797+
```toml
798+
[files]
799+
max_serve_file_size = 2147483648 # 2 GB
800+
```
801+
802+
Or set to 0 to disable the limit entirely. This also applies via the `STATIC_FILES_MAX_SERVE_FILE_SIZE` environment variable.
803+
786804
### Files are stale after a deploy
787805
788806
The in-memory cache serves files from memory after the first request (or immediately if `preload = true`). After deploying new files to disk, flush both the file cache and the path-safety cache:

config.toml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ idle_timeout = "75s"
3232
# How long to wait for in-flight requests to complete during graceful shutdown.
3333
shutdown_timeout = "15s"
3434

35+
# Maximum concurrent connections allowed from a single IP address.
36+
# 0 means unlimited (default). Set to a positive value (e.g. 100) to limit
37+
# per-IP connection count and mitigate connection exhaustion attacks.
38+
# max_conns_per_ip = 0
39+
3540
[files]
3641
# Directory to serve files from.
3742
root = "./public"
@@ -42,6 +47,10 @@ index = "index.html"
4247
# Custom 404 page path, relative to root. Leave empty for the built-in 404.
4348
not_found = "404.html"
4449

50+
# Maximum file size (bytes) that will be served. Files exceeding this limit
51+
# receive a 413 Payload Too Large response. Default: 1 GB. Set to 0 to disable.
52+
# max_serve_file_size = 1073741824
53+
4554
[cache]
4655
# Enable or disable the in-memory LRU file cache.
4756
enabled = true
@@ -69,6 +78,11 @@ level = 5
6978
# Encoding priority: br > zstd > gzip
7079
precompressed = true
7180

81+
# Maximum response body size (bytes) eligible for on-the-fly compression.
82+
# Responses larger than this are sent uncompressed to avoid excessive memory use.
83+
# Default: 10 MB (10485760). Set to 0 to disable the limit.
84+
# max_compress_size = 10485760
85+
7286
[headers]
7387
# Glob pattern for fingerprinted/immutable assets (gets "Cache-Control: immutable").
7488
# Example: "**-[0-9a-f]*.{js,css}"

0 commit comments

Comments
 (0)