Skip to content

Hardening 2026-10: dependency upgrades, security & HTTP correctness fixes - #14

Merged
jkyberneees merged 21 commits into
mainfrom
hardening/2026-10
Oct 27, 2026
Merged

jkyberneees merged 21 commits into
mainfrom
hardening/2026-10

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Dependency upgrades plus the fixes from a four-expert review panel (security, concurrency, HTTP semantics, SRE). The full plan and its status are in docs/HARDENING_PLAN_2026-10.md. Each bug fix started with a regression test that failed by assertion.

Dependencies

  • fasthttp 1.70.0 → 1.77.0, klauspost/compress 1.18.5 → 1.20.3. Brotli now comes from the pure-Go go-brrr library; builds with CGO disabled and on Windows were checked. govulncheck reports nothing.

Security and data integrity

  • Multi-range amplification: a request with about 1300 0- ranges used to build a 133 MB response for a 100 KB file. Ranges are now capped at 16, a request whose ranges add up to more than the file is ignored (200), and an unsatisfiable range returns 416 with Content-Range: bytes */N.
  • A symlinked index.html could expose files outside the root, or dotfiles. Index paths now go through PathSafe.
  • PathCache cached results for paths that did not exist yet, so a later symlink could escape the root. Only paths that were verified to exist are cached now.
  • 206 responses were gzipped on the fly. Compression now applies to 200 responses only, never to streamed bodies.
  • Large files are streamed from disk and get ETag, Cache-Control and 304 support. HEAD no longer reads the body. Sidecar files larger than their source are ignored.
  • A listen failure or a bad TLS cert now exits with code 1. Before, the process kept running and served nothing, or panicked.

HTTP correctness

  • Vary tokens are merged into one header, so Vary: Origin is no longer dropped.
  • 304 responses carry Cache-Control, ETag and Vary.
  • HEAD reports the same Content-Length as GET when content is compressed.
  • If-Range is supported, and ETags are compared weakly.
  • OPTIONS returns 204 with Allow, and 405 responses include Allow.
  • Accept-Encoding names are matched case-insensitively and q-values are parsed as numbers.
  • Embedded default assets now return 304 when appropriate.

Robustness and operations

  • Preload stats the symlink target instead of the link. Expired cache entries are removed only if they are still the same entry, which closes a race.
  • New Config.Validate(). Invalid env values cause an error instead of being ignored, and an explicit --config path must exist.
  • SIGHUP reload keeps the CLI flags and the positional root.
  • IPv6 and hostname listeners work. tcp4 is still used for IPv4, to keep the documented throughput.
  • Added STATIC_SECURITY_HSTS_MAX_AGE, STATIC_SECURITY_DIRECTORY_LISTING and STATIC_SECURITY_HSTS_INCLUDE_SUBDOMAINS.
  • CI pins govulncheck and staticcheck, and adds gofmt, go mod tidy -diff and a config-example diff check. Added Dependabot. Release permissions are now scoped to the release job.

Behaviour changes

  • Invalid env or config values, and a missing explicit --config, now stop startup.
  • Unsatisfiable ranges return 416. A plain OPTIONS request returns 204.

Test plan

  • gofmt, go vet, go test -race ./..., staticcheck v0.8.2, go mod tidy -diff
  • Smoke test of the built binary:
    • range flood returns 200 at file size
    • 416 for an unsatisfiable range
    • a 206 has no Content-Encoding
    • 304 for a matching ETag
    • OPTIONS returns 204 with Allow
    • 405 includes Allow
    • a single Vary header
    • busy port and missing --config both exit 1
  • CI green

🤖 Generated with Claude Code

jkyberneees and others added 18 commits October 27, 2026 20:26
fasthttp now uses molecule-man/go-brrr (pure Go) instead of
andybalholm/brotli; CGO_ENABLED=0 and windows builds verified,
govulncheck clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Start now loads and validates the TLS keypair with tls.X509KeyPair
before opening any socket (OPS-002, replaces the panicking
mustReadFile), binds listeners synchronously and returns bind errors
directly, closing any listener already opened (OPS-001). Serve-loop
errors after startup are delivered on Errors(); errors caused by
Shutdown are suppressed.

Listeners use "tcp" instead of "tcp4" so IPv6 and dual-stack
addresses work (OPS-003). Listener fields are published under a mutex
and exposed via HTTPAddr/HTTPSAddr, removing the data race on
httpLn/httpsLn (OPS-004).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Validate rejects out-of-range or inconsistent settings (bad listen
  addresses, tls_cert without tls_key, negative timeouts/sizes, zero
  cache limits when caching is enabled, gzip level outside 1..9/-1,
  negative max-ages) and reports every problem at once (OPS-007).
- Load returns an error for env vars that fail to parse instead of
  silently dropping them; booleans use strconv.ParseBool (OPS-005).
- Load errors when a non-empty path does not exist (OPS-006).
- Implement documented STATIC_SECURITY_HSTS_MAX_AGE,
  STATIC_SECURITY_DIRECTORY_LISTING and add
  STATIC_SECURITY_HSTS_INCLUDE_SUBDOMAINS (OPS-008).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- serve() returns an exit code; a Start error (port in use, bad TLS
  keypair) exits 1, and a serve loop dying later cancels the signal
  handler context and exits 1 (OPS-001).
- An explicit --config that does not exist is an error; the implicit
  default config.toml remains optional (OPS-006).
- Config is built by loadConfig (file, env, CLI flags, positional root,
  Validate) both at startup and on SIGHUP, so reloads no longer drop
  CLI overrides (OPS-009). RunSignalHandler takes a ReloadFunc, no
  longer panics with a nil cache, and logs what reload actually does.
- parseBytes rejects sizes that overflow int64 (OPS-010).
- helpText no longer prints a literal %% (OPS-011).
- Sync embedded config.toml.example with the root example and test
  that they stay identical and valid (OPS-008).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the documented tcp4 fast path for empty/IPv4 hosts but select
"tcp" for IPv6 literals and hostnames so they no longer fail to bind
(OPS-003). SO_REUSEPORT/TCP_NODELAY on Linux apply to both families.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CI runs on push to main and on pull_request only (no double runs),
  with per-ref concurrency.
- Add gofmt, go mod tidy -diff, embedded config sync, and pinned
  staticcheck (v0.8.2) steps; pin govulncheck to v1.9.0.
- Annotate action SHAs with exact versions (checkout v4.3.1,
  setup-go v5.6.0).
- release.yml: default contents: read, contents: write scoped to the
  release job.
- .goreleaser.yaml: drop the go mod tidy before-hook (CI enforces
  tidiness) and use v2 archives.format_overrides.formats.
- Add weekly dependabot updates for gomod and github-actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread internal/handler/file.go Fixed
Comment thread internal/handler/file.go Fixed
Comment thread internal/handler/file.go Fixed
jkyberneees and others added 2 commits October 27, 2026 20:57
go.mod's `go 1.26.0` directive made setup-go pin Go 1.26.0, which has
31 known stdlib vulnerabilities (govulncheck failed). CI and release now
use the newest stable release (check-latest); go.mod keeps the minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ersion

Adds in-function Clean+HasPrefix containment checks before opening large
files and sidecars (defense in depth; resolves CodeQL go/path-injection),
opens sidecars once and size-checks the handle (no stat/read TOCTOU), and
bounds the stream length before converting int64 to int (32-bit safety).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread internal/handler/file.go Fixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit 80e9777 into main Oct 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants