Security
Security Hardening v1.5.1
Comprehensive security hardening of the entire GateControl project. Based on a full security audit with 39 identified issues across 4 areas: Authentication, Input Validation, Docker/Infrastructure, and Frontend.
Overview
| Severity | Found | Fixed | By Design |
|---|---|---|---|
| CRITICAL | 6 | 6 | — |
| HIGH | 12 | 12 | — |
| MEDIUM | 11 | 11 | — |
| LOW/INFO | 10 | 7 | 3 |
| Total | 39 | 36 | 3 |
CRITICAL Fixes
- #1 — Prototype Pollution CSRF Bypass:
req.tokenAuth,req.tokenId, andreq.tokenScopesare defensively reset inrequireAuth(). - #2 — Route-Auth Forward-Auth without Header:
/route-auth/verifyreturns401 Unauthorizedwhenx-route-domainis missing. - #3 — Caddy Config Injection: Header names/values validated,
rate_limit_windowallowlist,sticky_cookie_nameregex. - #4 — DNS-Check SSRF: Domain validation before DNS lookup, resolved IPs not included in response.
- #5 — Node as Root: Intentionally kept — WireGuard CLI requires root, container isolation is the security boundary.
- #6 — Key-File Permissions: After
chown -R, secret files are reset toroot:rootwithchmod 600.
HIGH Fixes
- #7 — Route-Auth Lockout: Changed from IP-based to email-based (prevents IP rotation bypass).
- #8 — OTP Range: Full range 000000–999999 with
padStart. - #9 — OTP Resend: Requires valid pending 2FA session.
- #10 — Route-Auth CSRF Key: Dedicated HMAC-derived key instead of shared app secret.
- #11 — WireGuard Config Injection: DNS validated as IP list, keepalive as integer, newlines blocked.
- #12 — Email HTML Injection: All interpolated values in email templates escaped.
- #13 — Route Target SSRF: Private/loopback IPs blocked as direct route targets (peer-linked routes not affected).
- #14 — Metrics Token Leak:
?token=query parameter removed, header auth only. - #15 — WG Key in Logs: wg-quick output filtered.
- #16 — Trust Proxy: Restricted to loopback.
- #17 — CSP Styles: Split into
style-src-elem(nonce) andstyle-src-attr(inline). - #18 — Dashboard XSS: API integers with
parseIntandtextContent.
MEDIUM Fixes
- #19 — TOTP Replay Prevention: In-memory tracking of used codes (90s expiry).
- #20 — Session Secure Warning: Warning in production without HTTPS.
- #21 — Rate-Limiter Bypass: Increased limit only for session auth.
- #22 — Backup Key Validation: Regex allowlist for settings keys.
- #23 — IP Filter Fix:
req.ipinstead of raw X-Forwarded-For. - #24 — CSS Injection: Peer group color validated against hex regex.
- #25 — Monitoring XSS: Response time sanitized with
parseInt. - #26 — API Key Masking: ip2location key not in DOM, shows "Key is set".
- #27 — Health Endpoint: Details only for localhost.
- #28 — WG Signal Handling: Guard variable prevents race condition.
LOW/INFO Fixes
| # | Fix |
|---|---|
| #30 | Rate-limit error strings i18n (EN+DE) |
| #31 | Hardcoded German strings replaced with i18n |
| #32 | Dead code removed |
| #33 | Argon2 parallelism reduced (4 → 1) |
| #37 | frame-ancestors: 'self' in CSP |
| #38 | Crypto split with length check |
| #39 | Branding fields: max 255/2000 characters |
Breaking Changes
#13 — Route Targets: Private IPs Blocked
Direct entry of private or loopback IP addresses as route targets is blocked. Affected: 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, 0.x.
Not affected: Routes with peer linking. The WireGuard peer IP is still accepted.
Migration: Create a WireGuard peer for the target device and select the peer in the route dropdown instead of entering the IP manually.
#14 — Prometheus: Query Parameter Auth Removed
?token=gc_xxx is no longer accepted. Header auth only:
scrape_configs:
- job_name: 'gatecontrol'
metrics_path: '/metrics'
authorization:
type: 'Bearer'
credentials: 'gc_abc123...'
static_configs:
- targets: ['gatecontrol.example.com:443']
scheme: https
Alternative: credentials_file for even more security.
Other Breaking Changes
| Fix | Impact |
|---|---|
| #7 | Lockout per email instead of IP |
| #10 | Route-auth CSRF tokens invalid once after update (reload the page) |
| #16 | Only loopback trusted as proxy |
| #26 | ip2location API key no longer visible in the UI |
| #27 | /health externally only returns {ok: true/false} |