Add the HTTP hardening the policy requires before 1.0 (closes #108)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Every answer carries the six security headers REPO_POLICIES.md lists. The Content-Security-Policy allows inline content only with a nonce made for that answer; the pages put it on their style and script blocks and drop their four style attributes. The request log and /ip/ take a forwarded address only from a proxy listed in TRUSTED_PROXIES, which stops the start when set but invalid: the last X-Forwarded-For entry that is not a trusted proxy. X-Real-IP and True-Client-IP are no longer read. The JSON routes and the health check send fixed messages in place of database, parser and resolver errors, and log the detail. Model: opus-5-5
This commit is contained in:
@@ -4,10 +4,12 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -32,6 +34,8 @@ var (
|
||||
errRelativeXDGDataHome = errors.New("XDG_DATA_HOME must be an absolute path")
|
||||
errUnsupportedOS = errors.New("unsupported operating system")
|
||||
errInvalidPort = errors.New("PORT must be a whole number")
|
||||
errInvalidProxy = errors.New(
|
||||
"TRUSTED_PROXIES must list IP addresses and CIDR ranges, comma-separated")
|
||||
)
|
||||
|
||||
// Config holds configuration for the entire application
|
||||
@@ -42,6 +46,10 @@ type Config struct {
|
||||
// Port is the TCP port the HTTP server listens on
|
||||
Port int
|
||||
|
||||
// TrustedProxies are the reverse proxies whose X-Forwarded-For header
|
||||
// the HTTP server believes
|
||||
TrustedProxies []netip.Prefix
|
||||
|
||||
// MaxRuntime is the maximum runtime (0 = run forever)
|
||||
MaxRuntime time.Duration
|
||||
|
||||
@@ -68,9 +76,15 @@ func New() (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trustedProxies, err := getTrustedProxies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Config{
|
||||
StateDir: stateDir,
|
||||
Port: port,
|
||||
StateDir: stateDir,
|
||||
Port: port,
|
||||
TrustedProxies: trustedProxies,
|
||||
// Run forever by default
|
||||
MaxRuntime: 0,
|
||||
// Enable batching by default
|
||||
@@ -145,6 +159,38 @@ func getPort() (int, error) {
|
||||
return int(port), nil
|
||||
}
|
||||
|
||||
// getTrustedProxies returns the addresses and CIDR ranges listed in
|
||||
// TRUSTED_PROXIES, such as "10.0.0.0/8,192.0.2.1", or none when it is not set.
|
||||
// An address is returned as a range holding only that address.
|
||||
func getTrustedProxies() ([]netip.Prefix, error) {
|
||||
value := os.Getenv("TRUSTED_PROXIES")
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var proxies []netip.Prefix
|
||||
|
||||
for entry := range strings.SplitSeq(value, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
|
||||
addr, err := netip.ParseAddr(entry)
|
||||
if err == nil {
|
||||
proxies = append(proxies, netip.PrefixFrom(addr, addr.BitLen()))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
prefix, err := netip.ParsePrefix(entry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w, got %q", errInvalidProxy, entry)
|
||||
}
|
||||
|
||||
proxies = append(proxies, prefix)
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
// EnsureDirectories creates all necessary directories if they don't exist
|
||||
func (c *Config) EnsureDirectories() error {
|
||||
// Ensure state directory exists
|
||||
|
||||
Reference in New Issue
Block a user