Harden UI: login brute-force throttle, security headers, Secure cookie flag

- New in-memory login throttle (handler/login_throttle.go): 5 failed
  attempts per IP or per username within 5 minutes locks that key out for
  5 minutes, applied to both /login and the TOTP verification step, which
  previously had no rate limiting at all
- router.New now adds middleware.Secure with X-Frame-Options,
  X-Content-Type-Options, Referrer-Policy, and HSTS (only when cookies
  are Secure, implying an HTTPS deployment). No CSP: the existing
  templates rely on inline <script> blocks, so a CSP strict enough to
  matter would need 'unsafe-inline' anyway
- All session/auth cookies now set Secure based on the new
  --cookie-secure flag / WGUI_COOKIE_SECURE env var (default true) so the
  session cookie is never sent over plain HTTP unless explicitly opted
  into an HTTP-only LAN deployment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
This commit is contained in:
sysops
2026-07-29 15:03:21 +02:00
co-authored by Claude Sonnet 5
parent 1539c589a1
commit 4d171b4ff7
7 changed files with 251 additions and 0 deletions
+24
View File
@@ -48,6 +48,17 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
return tmpl.ExecuteTemplate(w, "base.html", data)
}
// hstsMaxAge returns a one-year HSTS max-age when cookies are marked Secure
// (implying an HTTPS deployment), or 0 (no HSTS header at all) otherwise -
// sending HSTS over a deliberately HTTP-only deployment would be pointless
// and could lock out an admin who later can't reach it over HTTPS.
func hstsMaxAge(cookieSecure bool) int {
if cookieSecure {
return 31536000
}
return 0
}
// New function
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo {
e := echo.New()
@@ -57,6 +68,7 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
cookieStore := sessions.NewCookieStore(secret[:32], secret[32:])
cookieStore.Options.Path = cookiePath
cookieStore.Options.HttpOnly = true
cookieStore.Options.Secure = util.CookieSecure
cookieStore.MaxAge(86400 * 7)
e.Use(session.Middleware(cookieStore))
@@ -166,6 +178,18 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
e.Logger.SetLevel(lvl)
e.Pre(middleware.RemoveTrailingSlash())
e.Use(middleware.LoggerWithConfig(logConfig))
// Basic hardening headers. No Content-Security-Policy here: the
// existing templates rely heavily on inline <script> blocks, and a CSP
// strict enough to matter would need 'unsafe-inline' anyway, making it
// mostly cosmetic - not worth the risk of quietly breaking the UI.
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
ReferrerPolicy: "same-origin",
HSTSMaxAge: hstsMaxAge(util.CookieSecure),
HSTSExcludeSubdomains: false,
}))
e.HideBanner = true
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
e.Validator = NewValidator()