Honour forwarded headers only from trusted proxies (closes #259)
check / check (push) Failing after 7s
check / check (push) Failing after 7s
A new ClientIP middleware sets request.RemoteAddr to the client's address before the request is logged. X-Forwarded-For and X-Real-IP count only when the peer is in TRUSTED_PROXIES, which defaults to the RFC 1918 ranges. The login rate limit, the session's IP address and hostname, and the request log all use that address. Model: opus-5-5
This commit is contained in:
1 parent
bc8aa6bb96
commit
c7ea02ea54
10 files changed
+670
-46
No files matched your search
@@ -1,11 +1,14 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
@@ -140,8 +143,7 @@ func settingsOf(cfg *config.Config) settings {
|
||||
}
|
||||
|
||||
func TestBadSettingStopsStartup(t *testing.T) {
|
||||
// For each setting, the first value does not parse and the others
|
||||
// parse but are out of range.
|
||||
// Each value is invalid for its setting.
|
||||
tests := []struct {
|
||||
key string
|
||||
values []string
|
||||
@@ -158,6 +160,10 @@ func TestBadSettingStopsStartup(t *testing.T) {
|
||||
{key: "MESSAGE_MAX_AGE", values: []string{"30d", "0", "-1h"}},
|
||||
{key: "QUEUE_MAX_AGE", values: []string{"720", "0s", "-24h"}},
|
||||
{key: "SESSION_IDLE_TIMEOUT", values: []string{"a month", "0", "-1m"}},
|
||||
{key: "TRUSTED_PROXIES", values: []string{
|
||||
"proxy.example.com", "10.0.0.0/33", "192.0.2.1:8080",
|
||||
"::ffff:10.0.0.0/104", "fe80::1%eth0",
|
||||
}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -206,3 +212,124 @@ func TestIRCListenAddrEmptyTurnsListenerOff(t *testing.T) {
|
||||
t.Errorf("IRCListenAddr = %q, want empty", cfg.IRCListenAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedProxies(t *testing.T) {
|
||||
// Each case lists addresses that must be in the trusted set and
|
||||
// addresses that must not.
|
||||
tests := []struct {
|
||||
name string
|
||||
set bool
|
||||
value string
|
||||
trusted []string
|
||||
untrusted []string
|
||||
}{
|
||||
{
|
||||
name: "unset trusts the RFC 1918 ranges",
|
||||
set: false,
|
||||
value: "",
|
||||
trusted: []string{
|
||||
"10.1.2.3", "172.16.0.1", "172.31.255.254", "192.168.1.1",
|
||||
},
|
||||
untrusted: []string{
|
||||
"127.0.0.1", "172.32.0.1", "192.0.2.1", "2001:db8::1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "a value replaces the default",
|
||||
set: true,
|
||||
value: "198.51.100.0/24, 192.0.2.7,2001:db8::/32",
|
||||
trusted: []string{"198.51.100.200", "192.0.2.7", "2001:db8::5"},
|
||||
untrusted: []string{"10.4.5.6", "192.168.4.4", "192.0.2.8"},
|
||||
},
|
||||
{
|
||||
name: "a mapped IPv6 address is trusted as IPv4",
|
||||
set: true,
|
||||
value: "::ffff:192.0.2.7",
|
||||
trusted: []string{"192.0.2.7"},
|
||||
untrusted: []string{"10.7.8.9"},
|
||||
},
|
||||
{
|
||||
name: "the empty string trusts nothing",
|
||||
set: true,
|
||||
value: "",
|
||||
trusted: nil,
|
||||
untrusted: []string{"10.0.0.1", "192.168.0.1", "::1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// t.Setenv puts back whatever value was there after the
|
||||
// test, including when Unsetenv removes it.
|
||||
t.Setenv("TRUSTED_PROXIES", test.value)
|
||||
|
||||
if !test.set {
|
||||
err := os.Unsetenv("TRUSTED_PROXIES")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
proxies := newConfig(t).TrustedProxies
|
||||
|
||||
for _, addr := range test.trusted {
|
||||
if !proxies.Contains(netip.MustParseAddr(addr)) {
|
||||
t.Errorf("%s is not trusted", addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range test.untrusted {
|
||||
if proxies.Contains(netip.MustParseAddr(addr)) {
|
||||
t.Errorf("%s is trusted", addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedProxiesNotAStringInYAMLStopsStartup(t *testing.T) {
|
||||
// YAML reads the first value as a list and the second as a map.
|
||||
values := []string{"[10.0.0.1]", "{proxy: 10.0.0.1}"}
|
||||
|
||||
for _, value := range values {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
// t.Setenv puts back whatever value was there after the
|
||||
// test, including when Unsetenv removes it.
|
||||
t.Setenv("TRUSTED_PROXIES", "")
|
||||
|
||||
err := os.Unsetenv("TRUSTED_PROXIES")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// loadConfig names the app neoirc-test, so config.New reads
|
||||
// $HOME/.config/neoirc-test/neoirc-test.yaml.
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
||||
dir := filepath.Join(home, ".config", "neoirc-test")
|
||||
|
||||
err = os.MkdirAll(dir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(
|
||||
filepath.Join(dir, "neoirc-test.yaml"),
|
||||
[]byte("TRUSTED_PROXIES: "+value+"\n"), 0o600,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Viper keeps the values it read from this file after the
|
||||
// file is gone. Reset clears them for later tests.
|
||||
t.Cleanup(viper.Reset)
|
||||
|
||||
_, err = loadConfig(t)
|
||||
if err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXIES") {
|
||||
t.Errorf("error = %v, want one naming TRUSTED_PROXIES", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user