Header-only C++ library; CMake config package; zero-coupling files lifted from fewo-webapp: interceptor/SecurityHeadersInterceptor.hpp interceptor/BodySizeLimitInterceptor.hpp handler/JsonErrorHandler.hpp util/RateLimiter.hpp util/TokenExtract.hpp (extractToken, isValidIp, clientIpTrusted) startup/RequireEncryptionKey.hpp fewo-specific couplings (bindAddress global, fewo::config) replaced with explicit function arguments so the library stands alone. AuthInterceptor + requireAdmin deferred to v0.2 — they need IAuthBackend / IAuthPolicy / IRuntimeConfig seams designed first. docs/security-baseline.md ships CSP / rate-limit / body-size / encryption key constants as language-neutral baselines for non-C++ consumers. Closes fewo-webapp#412 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
2.7 KiB
Markdown
48 lines
2.7 KiB
Markdown
# Security baseline — language-neutral constants
|
|
|
|
These are the defaults enforced by the C++ interceptors and utilities. Consumers
|
|
on other stacks (including palibu's C backend, while it's still pre-migration)
|
|
should mirror the same values rather than re-deriving them.
|
|
|
|
## Response headers (SecurityHeadersInterceptor)
|
|
|
|
| Header | Value | Why |
|
|
|--------|-------|-----|
|
|
| `Content-Security-Policy` | `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'` | Strict by default. Opt in to `https://unpkg.com` only for the Swagger UI route on apps that serve it. |
|
|
| `X-Frame-Options` | `DENY` | Defence in depth against clickjacking (CSP `frame-ancestors` is primary). |
|
|
| `X-Content-Type-Options` | `nosniff` | |
|
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` | |
|
|
| `Permissions-Policy` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` | Disable sensors the apps don't need. |
|
|
|
|
## Request limits (BodySizeLimitInterceptor)
|
|
|
|
- **Max body size:** 1 MiB (1,048,576 bytes). Fail with HTTP 413 (`Payload Too Large`).
|
|
- **Paths exempt:** none in the baseline. Upload-heavy apps override per-route.
|
|
|
|
## Rate limiting (RateLimiter)
|
|
|
|
- **Key:** `clientIpTrusted(request, bindAddress)` — returns a validated IPv4/IPv6
|
|
string, or `"invalid"`/`"unknown"`. Never attacker-chosen free-form text.
|
|
- **Default bucket:** 60 requests / 60 seconds per key. Tune per endpoint.
|
|
- **Loopback-only XFF trust:** `X-Forwarded-For` / `X-Real-IP` are honoured only
|
|
when the service binds to `127.0.0.1` / `::1` / `localhost`. On a public
|
|
interface we fall back to `"unknown"` (all untagged callers share one bucket,
|
|
which is stricter than letting forged headers dilute it).
|
|
|
|
## Encryption key gate (startup::requireEncryptionKey)
|
|
|
|
- **Required env var** (name is consumer-chosen; fewo uses `FEWO_ENCRYPTION_KEY`,
|
|
the scaffold standard is `APP_ENCRYPTION_KEY`): symmetric key, SHA-256 derives
|
|
the actual AES-256-GCM key.
|
|
- **Startup behaviour:** missing key + no `--allow-plaintext` dev flag ⇒ refuse
|
|
to start (throws). Missing key + `--allow-plaintext` ⇒ WARN and continue with
|
|
plaintext storage.
|
|
- **Rationale:** prevents PII / credentials silently landing in plaintext
|
|
production databases when an operator forgets to populate `/etc/<name>/prod.env`.
|
|
|
|
## Session cookie (not in library v0.1 — convention documented here)
|
|
|
|
- `Set-Cookie: session=<token>; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000`
|
|
(30 days). Add `Secure` when serving behind TLS.
|
|
- Token: 32 random bytes base64url-encoded. Stored in the session table hashed
|
|
(SHA-256) so DB compromise doesn't yield session hijacking.
|