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:
@@ -3,15 +3,18 @@ package main
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -29,7 +32,8 @@ type App struct {
|
||||
geo *Geo // nil in tests
|
||||
updates *Updater // nil in tests
|
||||
started time.Time
|
||||
shutdown func() // graceful stop; systemd restarts the service
|
||||
shutdown func() // graceful stop; systemd restarts the service
|
||||
webAddrs []string // the addresses the web server listens on now
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
@@ -1090,11 +1094,15 @@ func (a *App) patchSettings(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := json.Marshal(w)
|
||||
return string(b)
|
||||
}
|
||||
before := listen()
|
||||
before, oldWeb := listen(), c.Web
|
||||
if err := field(m, "web", &c.Web); err != nil {
|
||||
return err
|
||||
}
|
||||
restart = listen() != before
|
||||
if restart = listen() != before; restart {
|
||||
if err := a.checkWebStart(oldWeb, c.Web); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := field(m, "stats", &c.Stats); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1236,7 +1244,20 @@ func (a *App) restore(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, badRequest("this file has no server key; is it a backup of this app?"))
|
||||
return
|
||||
}
|
||||
if err := a.store.Update(func(c *Config) error { *c = in; return nil }); err != nil {
|
||||
if in.Version > configVersion {
|
||||
writeErr(w, badRequest("this backup is from a newer version of %s; update this server first", appName))
|
||||
return
|
||||
}
|
||||
in.applyDefaults()
|
||||
if !in.passwordSet() {
|
||||
writeErr(w, badRequest("this backup has no user with a password; restoring it would lock everyone out"))
|
||||
return
|
||||
}
|
||||
if err := a.store.Update(func(c *Config) error {
|
||||
old := c.Web
|
||||
*c = in
|
||||
return a.checkWebStart(old, c.Web)
|
||||
}); err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
@@ -1244,6 +1265,55 @@ func (a *App) restore(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "applyError": a.apply(), "restartRequired": true})
|
||||
}
|
||||
|
||||
// checkWebStart refuses web settings the service could not start with: an
|
||||
// address it cannot listen on, or certificate files it cannot read. The
|
||||
// service would stop at the next restart, and the web interface and the API
|
||||
// with it.
|
||||
func (a *App) checkWebStart(old, next WebConfig) error {
|
||||
if err := validateListen(next.Listen, "listen address", false); err != nil {
|
||||
return &userError{err.Error()}
|
||||
}
|
||||
if err := validateListen(next.HTTPListen, "HTTP listen address", true); err != nil {
|
||||
return &userError{err.Error()}
|
||||
}
|
||||
if next.TLS.Mode == "files" && next.TLS != old.TLS {
|
||||
if _, err := tls.LoadX509KeyPair(next.TLS.CertFile, next.TLS.KeyFile); err != nil {
|
||||
return badRequest("the certificate files cannot be used: %v", err)
|
||||
}
|
||||
}
|
||||
addrs := []string{next.Listen}
|
||||
if next.HTTPListen != "" && next.TLS.Mode != "off" {
|
||||
addrs = append(addrs, next.HTTPListen)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if err := a.canListen(addr); err != nil {
|
||||
return badRequest("cannot listen on %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// canListen tries to listen on addr. An address the service listens on now,
|
||||
// or one whose port it holds, is fine: it is free again after the restart.
|
||||
func (a *App) canListen(addr string) error {
|
||||
if slices.Contains(a.webAddrs, addr) {
|
||||
return nil
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err == nil {
|
||||
return ln.Close()
|
||||
}
|
||||
if errors.Is(err, syscall.EADDRINUSE) {
|
||||
_, port, _ := net.SplitHostPort(addr)
|
||||
for _, own := range a.webAddrs {
|
||||
if _, p, _ := net.SplitHostPort(own); p == port {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// applyRuntime applies the settings that take effect without a restart: log
|
||||
// level and log rotation. Traffic retention is read by the stats sampler.
|
||||
func (a *App) applyRuntime(c *Config) {
|
||||
|
||||
Reference in New Issue
Block a user