Fixes from the audit: input checks, apply order, sign-in limits
- The server endpoint must be a plain host name or IP address. It is written into client configs as is, so a newline could add lines such as PreUp, which wg-quick runs as root on the client. - Listen addresses and the session length (1–720 hours) are checked. Before web settings or a restore are saved, the server tries the new listen addresses and certificate files, so a value it cannot start with is refused instead of stopping the service at the next restart. - Kernel applies run one at a time and read the config once it is their turn, so an older config can no longer be applied last. - Pending passkey sign-ins are capped: 10 per address, 1000 in total. - Behind a local proxy, the last X-Forwarded-For entry is the client; earlier ones come from the client and are ignored. - With LAN access off, peers are also kept from the IPv6 networks on the uplink, not only from its private IPv4 networks. - A change that leaves no user with a password is refused, and so is a backup without one or from a newer version.
This commit is contained in:
@@ -217,6 +217,7 @@ type ticket struct {
|
||||
|
||||
type ceremony struct {
|
||||
userID string // "" for a passkey sign-in
|
||||
ip string // lockKey of who started a passkey sign-in
|
||||
data *webauthn.SessionData
|
||||
expires time.Time
|
||||
}
|
||||
@@ -551,12 +552,52 @@ func (a *App) loginPasskeyBegin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
id := randomString(24)
|
||||
ip := remoteIP(r)
|
||||
a.auth.mu.Lock()
|
||||
a.auth.mfa.logins[id] = &ceremony{data: data, expires: time.Now().Add(ticketTTL)}
|
||||
ok := a.auth.addPasskeyLoginLocked(id, &ceremony{data: data, ip: lockKey(ip), expires: time.Now().Add(ticketTTL)})
|
||||
a.auth.mu.Unlock()
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": errBusy.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"id": id, "options": opts})
|
||||
}
|
||||
|
||||
// Anyone can start a passkey sign-in, so the pending ones are capped: per
|
||||
// address, and in total, where the oldest makes room.
|
||||
const (
|
||||
maxPasskeyLogins = 1000
|
||||
maxPasskeyLoginsPerIP = 10
|
||||
)
|
||||
|
||||
// addPasskeyLoginLocked stores a started passkey sign-in, or reports false
|
||||
// when its address has too many pending. a.mu must be held.
|
||||
func (a *Auth) addPasskeyLoginLocked(id string, c *ceremony) bool {
|
||||
now := time.Now()
|
||||
var fromIP int
|
||||
var oldestID string
|
||||
for k, x := range a.mfa.logins {
|
||||
if now.After(x.expires) {
|
||||
delete(a.mfa.logins, k)
|
||||
continue
|
||||
}
|
||||
if x.ip == c.ip {
|
||||
fromIP++
|
||||
}
|
||||
if oldestID == "" || x.expires.Before(a.mfa.logins[oldestID].expires) {
|
||||
oldestID = k
|
||||
}
|
||||
}
|
||||
if fromIP >= maxPasskeyLoginsPerIP {
|
||||
return false
|
||||
}
|
||||
if len(a.mfa.logins) >= maxPasskeyLogins {
|
||||
delete(a.mfa.logins, oldestID)
|
||||
}
|
||||
a.mfa.logins[id] = c
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) loginPasskeyFinish(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
ip := remoteIP(r)
|
||||
|
||||
Reference in New Issue
Block a user