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:
co-authored by
Claude Sonnet 5
parent
1539c589a1
commit
4d171b4ff7
@@ -3813,3 +3813,94 @@ Keine Commits in dieser Session.
|
|||||||
- wireguard/service.go | 23 ++++++++++++-----
|
- wireguard/service.go | 23 ++++++++++++-----
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-07-29 14:29 – 14:31 (2m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 1539c58 Normalize AllocatedIPs/ExtraAllowedIPs to network address before ip route replace
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:34 – 14:34 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:35 – 14:36 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:39 – 14:39 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:40 – 14:40 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:40 – 14:41 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-07-29 14:41 – 14:41 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** wireguard-ui-multi
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- templates/wg.conf | 2 +-
|
||||||
|
- util/util.go | 21 +++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loginThrottle tracks failed login attempts per key (username and/or
|
||||||
|
// client IP) in memory and locks a key out for a fixed window once it
|
||||||
|
// crosses maxFailedAttempts. There is no persistent brute-force
|
||||||
|
// protection anywhere else in the login path (see Login in routes.go),
|
||||||
|
// so an attacker could otherwise try passwords as fast as the network
|
||||||
|
// allows.
|
||||||
|
type loginThrottle struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
attempts map[string]*throttleEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type throttleEntry struct {
|
||||||
|
failures int
|
||||||
|
lockedUntil time.Time
|
||||||
|
windowStart time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxFailedAttempts = 5
|
||||||
|
failureWindow = 5 * time.Minute
|
||||||
|
lockoutDuration = 5 * time.Minute
|
||||||
|
throttleSweepEvery = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var globalLoginThrottle = &loginThrottle{attempts: make(map[string]*throttleEntry)}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go globalLoginThrottle.sweepLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sweepLoop periodically drops stale entries so the map doesn't grow
|
||||||
|
// unbounded on a long-running instance under sustained scanning.
|
||||||
|
func (t *loginThrottle) sweepLoop() {
|
||||||
|
for {
|
||||||
|
time.Sleep(throttleSweepEvery)
|
||||||
|
now := time.Now()
|
||||||
|
t.mu.Lock()
|
||||||
|
for k, e := range t.attempts {
|
||||||
|
if now.After(e.lockedUntil) && now.Sub(e.windowStart) > failureWindow {
|
||||||
|
delete(t.attempts, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow reports whether key (typically remote IP, optionally combined
|
||||||
|
// with the attempted username) is currently allowed to attempt a login.
|
||||||
|
func (t *loginThrottle) allow(key string) bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
e, ok := t.attempts[key]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return time.Now().After(e.lockedUntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordFailure registers a failed attempt for key, locking it out once
|
||||||
|
// maxFailedAttempts is reached within failureWindow.
|
||||||
|
func (t *loginThrottle) recordFailure(key string) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
e, ok := t.attempts[key]
|
||||||
|
if !ok || now.Sub(e.windowStart) > failureWindow {
|
||||||
|
e = &throttleEntry{windowStart: now}
|
||||||
|
t.attempts[key] = e
|
||||||
|
}
|
||||||
|
e.failures++
|
||||||
|
if e.failures >= maxFailedAttempts {
|
||||||
|
e.lockedUntil = now.Add(lockoutDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordSuccess clears any throttle state for key on a successful login,
|
||||||
|
// so a legitimate user isn't penalized by earlier typos once they get it
|
||||||
|
// right.
|
||||||
|
func (t *loginThrottle) recordSuccess(key string) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
delete(t.attempts, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tooManyRequests writes a 429 response for a throttled login attempt.
|
||||||
|
func tooManyRequests(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
|
_, err := w.Write([]byte(`{"success":false,"message":"Too many failed login attempts. Please wait a few minutes and try again."}`))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -116,9 +116,18 @@ func Login(db store.IStore) echo.HandlerFunc {
|
|||||||
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipKey := "ip:" + c.RealIP()
|
||||||
|
userKey := "user:" + username
|
||||||
|
if !globalLoginThrottle.allow(ipKey) || !globalLoginThrottle.allow(userKey) {
|
||||||
|
log.Warnf("Login throttled for user %s (%s)", username, c.Request().RemoteAddr)
|
||||||
|
return tooManyRequests(c.Response())
|
||||||
|
}
|
||||||
|
|
||||||
dbuser, err := db.GetUserByName(username)
|
dbuser, err := db.GetUserByName(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Invalid credentials. Cannot query user %s from DB (%s)", username, c.Request().RemoteAddr)
|
log.Warnf("Invalid credentials. Cannot query user %s from DB (%s)", username, c.Request().RemoteAddr)
|
||||||
|
globalLoginThrottle.recordFailure(ipKey)
|
||||||
|
globalLoginThrottle.recordFailure(userKey)
|
||||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"})
|
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +145,8 @@ func Login(db store.IStore) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userCorrect && passwordCorrect {
|
if userCorrect && passwordCorrect {
|
||||||
|
globalLoginThrottle.recordSuccess(ipKey)
|
||||||
|
globalLoginThrottle.recordSuccess(userKey)
|
||||||
if dbuser.TOTPEnabled {
|
if dbuser.TOTPEnabled {
|
||||||
// Password check passed, but this account requires a second
|
// Password check passed, but this account requires a second
|
||||||
// factor. Stash a short-lived, NOT-yet-authenticated pending
|
// factor. Stash a short-lived, NOT-yet-authenticated pending
|
||||||
@@ -147,6 +158,7 @@ func Login(db store.IStore) echo.HandlerFunc {
|
|||||||
Path: cookiePath,
|
Path: cookiePath,
|
||||||
MaxAge: 300, // 5 minutes to enter the TOTP code
|
MaxAge: 300, // 5 minutes to enter the TOTP code
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: util.CookieSecure,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
sess.Values["pending_totp_user"] = dbuser.Username
|
sess.Values["pending_totp_user"] = dbuser.Username
|
||||||
@@ -165,6 +177,8 @@ func Login(db store.IStore) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Warnf("Invalid credentials user %s (%s)", username, c.Request().RemoteAddr)
|
log.Warnf("Invalid credentials user %s (%s)", username, c.Request().RemoteAddr)
|
||||||
|
globalLoginThrottle.recordFailure(ipKey)
|
||||||
|
globalLoginThrottle.recordFailure(userKey)
|
||||||
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid credentials"})
|
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid credentials"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +200,7 @@ func finalizeLoginSession(c echo.Context, dbuser model.User, rememberMe bool) er
|
|||||||
Path: cookiePath,
|
Path: cookiePath,
|
||||||
MaxAge: ageMax,
|
MaxAge: ageMax,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: util.CookieSecure,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +225,7 @@ func finalizeLoginSession(c echo.Context, dbuser model.User, rememberMe bool) er
|
|||||||
cookie.Value = tokenUID
|
cookie.Value = tokenUID
|
||||||
cookie.MaxAge = ageMax
|
cookie.MaxAge = ageMax
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
|
cookie.Secure = util.CookieSecure
|
||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
c.SetCookie(cookie)
|
c.SetCookie(cookie)
|
||||||
|
|
||||||
@@ -236,6 +252,13 @@ func VerifyTOTPLogin(db store.IStore) echo.HandlerFunc {
|
|||||||
|
|
||||||
code, _ := data["code"].(string)
|
code, _ := data["code"].(string)
|
||||||
|
|
||||||
|
ipKey := "ip:" + c.RealIP()
|
||||||
|
userKey := "totp:" + pendingUsername
|
||||||
|
if !globalLoginThrottle.allow(ipKey) || !globalLoginThrottle.allow(userKey) {
|
||||||
|
log.Warnf("TOTP login throttled for user %s (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||||
|
return tooManyRequests(c.Response())
|
||||||
|
}
|
||||||
|
|
||||||
dbuser, err := db.GetUserByName(pendingUsername)
|
dbuser, err := db.GetUserByName(pendingUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Invalid pending TOTP login. Cannot query user %s from DB (%s)", pendingUsername, c.Request().RemoteAddr)
|
log.Warnf("Invalid pending TOTP login. Cannot query user %s from DB (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||||
@@ -244,8 +267,12 @@ func VerifyTOTPLogin(db store.IStore) echo.HandlerFunc {
|
|||||||
|
|
||||||
if !dbuser.TOTPEnabled || !auth.Validate(dbuser.TOTPSecret, code, time.Now()) {
|
if !dbuser.TOTPEnabled || !auth.Validate(dbuser.TOTPSecret, code, time.Now()) {
|
||||||
log.Warnf("Invalid TOTP code for user %s (%s)", pendingUsername, c.Request().RemoteAddr)
|
log.Warnf("Invalid TOTP code for user %s (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||||
|
globalLoginThrottle.recordFailure(ipKey)
|
||||||
|
globalLoginThrottle.recordFailure(userKey)
|
||||||
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid code"})
|
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid code"})
|
||||||
}
|
}
|
||||||
|
globalLoginThrottle.recordSuccess(ipKey)
|
||||||
|
globalLoginThrottle.recordSuccess(userKey)
|
||||||
|
|
||||||
// clear the pending state before finalizing the real session
|
// clear the pending state before finalizing the real session
|
||||||
delete(sess.Values, "pending_totp_user")
|
delete(sess.Values, "pending_totp_user")
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ func doRefreshSession(c echo.Context) {
|
|||||||
Path: cookiePath,
|
Path: cookiePath,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: util.CookieSecure,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
sess.Save(c.Request(), c.Response())
|
sess.Save(c.Request(), c.Response())
|
||||||
@@ -152,6 +153,7 @@ func doRefreshSession(c echo.Context) {
|
|||||||
cookie.Value = oldCookie.Value
|
cookie.Value = oldCookie.Value
|
||||||
cookie.MaxAge = maxAge
|
cookie.MaxAge = maxAge
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
|
cookie.Secure = util.CookieSecure
|
||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
c.SetCookie(cookie)
|
c.SetCookie(cookie)
|
||||||
}
|
}
|
||||||
@@ -273,6 +275,7 @@ func clearSession(c echo.Context) {
|
|||||||
cookie.Path = cookiePath
|
cookie.Path = cookiePath
|
||||||
cookie.MaxAge = -1
|
cookie.MaxAge = -1
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
|
cookie.Secure = util.CookieSecure
|
||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
c.SetCookie(cookie)
|
c.SetCookie(cookie)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ var (
|
|||||||
flagWgConfTemplate string
|
flagWgConfTemplate string
|
||||||
flagBasePath string
|
flagBasePath string
|
||||||
flagSubnetRanges string
|
flagSubnetRanges string
|
||||||
|
flagCookieSecure = true
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -83,6 +84,7 @@ func init() {
|
|||||||
// command-line flags and env variables
|
// command-line flags and env variables
|
||||||
flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.")
|
flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.")
|
||||||
flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.")
|
flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.")
|
||||||
|
flag.BoolVar(&flagCookieSecure, "cookie-secure", util.LookupEnvOrBool(util.CookieSecureEnvVar, flagCookieSecure), "Set the Secure flag on the session cookie. Only disable for a deliberately HTTP-only LAN deployment.")
|
||||||
flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname")
|
flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname")
|
||||||
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
|
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
|
||||||
flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
|
flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
|
||||||
@@ -131,6 +133,7 @@ func init() {
|
|||||||
|
|
||||||
// update runtime config
|
// update runtime config
|
||||||
util.DisableLogin = flagDisableLogin
|
util.DisableLogin = flagDisableLogin
|
||||||
|
util.CookieSecure = flagCookieSecure
|
||||||
util.BindAddress = flagBindAddress
|
util.BindAddress = flagBindAddress
|
||||||
util.SmtpHostname = flagSmtpHostname
|
util.SmtpHostname = flagSmtpHostname
|
||||||
util.SmtpPort = flagSmtpPort
|
util.SmtpPort = flagSmtpPort
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
|
|||||||
return tmpl.ExecuteTemplate(w, "base.html", data)
|
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
|
// New function
|
||||||
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo {
|
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo {
|
||||||
e := echo.New()
|
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 := sessions.NewCookieStore(secret[:32], secret[32:])
|
||||||
cookieStore.Options.Path = cookiePath
|
cookieStore.Options.Path = cookiePath
|
||||||
cookieStore.Options.HttpOnly = true
|
cookieStore.Options.HttpOnly = true
|
||||||
|
cookieStore.Options.Secure = util.CookieSecure
|
||||||
cookieStore.MaxAge(86400 * 7)
|
cookieStore.MaxAge(86400 * 7)
|
||||||
|
|
||||||
e.Use(session.Middleware(cookieStore))
|
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.Logger.SetLevel(lvl)
|
||||||
e.Pre(middleware.RemoveTrailingSlash())
|
e.Pre(middleware.RemoveTrailingSlash())
|
||||||
e.Use(middleware.LoggerWithConfig(logConfig))
|
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.HideBanner = true
|
||||||
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
|
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
|
||||||
e.Validator = NewValidator()
|
e.Validator = NewValidator()
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ var (
|
|||||||
BasePath string
|
BasePath string
|
||||||
SubnetRanges map[string]([]*net.IPNet)
|
SubnetRanges map[string]([]*net.IPNet)
|
||||||
SubnetRangesOrder []string
|
SubnetRangesOrder []string
|
||||||
|
// CookieSecure sets the Secure flag on the session cookie, so browsers
|
||||||
|
// never send it over plain HTTP. Defaults to true; only disable it for
|
||||||
|
// a deliberately HTTP-only LAN deployment (see COOKIE_SECURE_ENV_VAR).
|
||||||
|
CookieSecure bool
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -70,6 +74,7 @@ const (
|
|||||||
DefaultClientExtraAllowedIpsEnvVar = "WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS"
|
DefaultClientExtraAllowedIpsEnvVar = "WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS"
|
||||||
DefaultClientUseServerDNSEnvVar = "WGUI_DEFAULT_CLIENT_USE_SERVER_DNS"
|
DefaultClientUseServerDNSEnvVar = "WGUI_DEFAULT_CLIENT_USE_SERVER_DNS"
|
||||||
DefaultClientEnableAfterCreationEnvVar = "WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION"
|
DefaultClientEnableAfterCreationEnvVar = "WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION"
|
||||||
|
CookieSecureEnvVar = "WGUI_COOKIE_SECURE"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseBasePath(basePath string) string {
|
func ParseBasePath(basePath string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user