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:
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -76,8 +77,29 @@ const (
|
||||
minLogFiles, maxLogFiles = 1, 100
|
||||
minHourlyHours, maxHourlyHrs = 24, 24 * 31
|
||||
minDailyDays, maxDailyDays = 7, 3660
|
||||
minSessionHours = 1
|
||||
maxSessionHours = 30 * 24
|
||||
)
|
||||
|
||||
// validateListen checks a listen address like ":443" or "192.0.2.1:443".
|
||||
// Empty is allowed when optional (the HTTP listener is then off).
|
||||
func validateListen(addr, field string, optional bool) error {
|
||||
if addr == "" && optional {
|
||||
return nil
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %q must look like :443 or 192.0.2.1:443", field, addr)
|
||||
}
|
||||
if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 {
|
||||
return fmt.Errorf("%s %q: the port must be 1–65535", field, addr)
|
||||
}
|
||||
if host != "" && host != "localhost" && checkEndpoint(host) != nil {
|
||||
return fmt.Errorf("%s %q: %q is not an IP address or host name", field, addr, host)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
Listen string `json:"listen"` // HTTPS (or HTTP when tls.mode is "off") listen address
|
||||
HTTPListen string `json:"httpListen"` // plain HTTP for ACME http-01 and redirects; "" disables
|
||||
@@ -364,7 +386,9 @@ func (c *Config) validate() error {
|
||||
if v6.Masked() != v6 {
|
||||
return fmt.Errorf("IPv6 network must be the network address, e.g. %s", v6.Masked())
|
||||
}
|
||||
if s.Endpoint != "" && strings.ContainsAny(s.Endpoint, " /:") && net.ParseIP(s.Endpoint) == nil {
|
||||
// The endpoint is written into client configs as is, so it must be a
|
||||
// plain host name or IP: anything else could add lines to them.
|
||||
if s.Endpoint != "" && checkEndpoint(s.Endpoint) != nil {
|
||||
return errors.New("endpoint must be a host name or IP address without port")
|
||||
}
|
||||
if err := validateHostList(s.ClientDefaults.DNS, "DNS", false); err != nil {
|
||||
@@ -397,6 +421,15 @@ func (c *Config) validate() error {
|
||||
if _, ok := updateSources[c.Updates.Source]; !ok {
|
||||
return fmt.Errorf("update source must be gitea or github")
|
||||
}
|
||||
if err := validateListen(c.Web.Listen, "listen address", false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateListen(c.Web.HTTPListen, "HTTP listen address", true); err != nil {
|
||||
return err
|
||||
}
|
||||
if h := c.Web.SessionHours; h < minSessionHours || h > maxSessionHours {
|
||||
return fmt.Errorf("session length must be %d–%d hours", minSessionHours, maxSessionHours)
|
||||
}
|
||||
switch c.Web.TLS.Mode {
|
||||
case "acme":
|
||||
if c.Web.TLS.Domain == "" {
|
||||
@@ -586,6 +619,12 @@ func (s *Store) Update(fn func(c *Config) error) error {
|
||||
s.mu.Unlock()
|
||||
return &userError{err.Error()}
|
||||
}
|
||||
// With no user left (applyDefaults then adds an "admin" without a
|
||||
// password), nobody could sign in until someone ran "passwd" on the server.
|
||||
if old.passwordSet() && !next.passwordSet() {
|
||||
s.mu.Unlock()
|
||||
return &userError{"this would leave no user with a password, and nobody could sign in"}
|
||||
}
|
||||
if err := writeFileAtomic(s.path, next, 0o600); err != nil {
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user