Compare commits

...
2 Commits
Author SHA1 Message Date
sneak 1740ac6061 sww tests: wait for every starting line as long as the start takes (closes #331)
check / check (push) Successful in 3m45s
Each test in internal/sww that waits for the starting line now waits until
the line is written or sww exits, so a slow Core Rule Set compile on a
loaded host no longer fails it. The two tests that run sww as a child
process get its exit status from a goroutine that waits on it, and no
longer kill it after 30 seconds; the waits after SIGTERM still end after
waitLimit. A cleanup still kills each child process as its test ends and
waits for it to exit. startRProxyProcess returns a stop function in place
of the process and its output.

Model: opus-5-5
2026-11-11 07:38:06 +00:00
clawbot 6e370b346b rproxy: remove forwarded headers spelt with underscores from an untrusted peer (closes #334)
check / check (push) Failing after 1m46s
For a peer outside `SWRP_TRUSTED_PROXIES`, `rproxy` now reads each header name with `_` as `-` and in any letter case, and removes the header when that name starts with `X-Forwarded-` or is `Forwarded` or `X-Real-IP`. Before, `X_Forwarded_Ssl: on` or `X_Real_IP` from a client connecting directly reached the app unchanged, and an app behind CGI or FastCGI reads them as the dash-spelt headers. A trusted peer's headers pass on as before. `SPEC.md` says so.

Model: opus-5-5
2026-11-11 08:29:59 +01:00
6 changed files with 81 additions and 61 deletions

No files matched your search

+2 -1
View File
@@ -2326,7 +2326,8 @@ but for these headers:
claims and are replaced. `X-Forwarded-For` names the peer, `X-Forwarded-Host` claims and are replaced. `X-Forwarded-For` names the peer, `X-Forwarded-Host`
the host it asked for, and `X-Forwarded-Proto` plain `http`. Every other the host it asked for, and `X-Forwarded-Proto` plain `http`. Every other
header whose name starts with `X-Forwarded-` is removed, and so are header whose name starts with `X-Forwarded-` is removed, and so are
`Forwarded` and `X-Real-IP`. `Forwarded` and `X-Real-IP`. A name spelt with `_` for `-`, such as
`X_Forwarded_Ssl`, counts as the same name.
- `X-Request-ID`, which carries the fresh id. - `X-Request-ID`, which carries the fresh id.
- `X-Client-ASN` and `X-Client-Country`, which carry what `waf` answered. A - `X-Client-ASN` and `X-Client-Country`, which carry what `waf` answered. A
header it did not give is left out. Any of the two the visitor sent is header it did not give is left out. Any of the two the visitor sent is
+5 -6
View File
@@ -105,17 +105,16 @@ func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) {
return return
} }
// ReverseProxy has removed Forwarded and the three set below. Go's server // An app behind CGI or FastCGI reads X_Forwarded_Ssl as X-Forwarded-Ssl, so
// gives header names in canonical form, so the prefix matches the name in // a name is matched with each _ read as - and in any letter case.
// any letter case.
for name := range out.Header { for name := range out.Header {
if strings.HasPrefix(name, "X-Forwarded-") { spelt := strings.ToLower(strings.ReplaceAll(name, "_", "-"))
if strings.HasPrefix(spelt, "x-forwarded-") ||
spelt == "forwarded" || spelt == "x-real-ip" {
delete(out.Header, name) delete(out.Header, name)
} }
} }
out.Header.Del("X-Real-IP")
out.Header.Set("X-Forwarded-For", forwardedFor) out.Header.Set("X-Forwarded-For", forwardedFor)
out.Header.Set("X-Forwarded-Host", in.Host) out.Header.Set("X-Forwarded-Host", in.Host)
out.Header.Set("X-Forwarded-Proto", "http") out.Header.Set("X-Forwarded-Proto", "http")
+2
View File
@@ -561,6 +561,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", "X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
"X-Forwarded-Scheme", "X-Forwarded-Ssl", "X-Forwarded-Protocol", "X-Forwarded-Scheme", "X-Forwarded-Ssl", "X-Forwarded-Protocol",
"X-Forwarded-Path", "X-Forwarded-Client-Cert", "X-Forwarded-Path", "X-Forwarded-Client-Cert",
"X_Forwarded_Ssl", "X_Forwarded_Proto", "X_Real_IP",
} }
standIn := startWAF(t, allow) standIn := startWAF(t, allow)
@@ -628,6 +629,7 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
forwardedProtoHeader: "https", forwardedProtoHeader: "https",
"X-Real-Ip": "198.51.100.1", "X-Real-Ip": "198.51.100.1",
"X-Forwarded-Ssl": "on", "X-Forwarded-Ssl": "on",
"X_Forwarded_Ssl": "on",
} }
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
+1 -1
View File
@@ -41,7 +41,7 @@ func TestHealthCheckInWAFsContainer(t *testing.T) {
exited <- run(ctx, settings, out) exited <- run(ctx, settings, out)
}() }()
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
_, port, _ := net.SplitHostPort(addr) _, port, _ := net.SplitHostPort(addr)
// The container's settings: an address to listen on with an empty // The container's settings: an address to listen on with an empty
+31 -35
View File
@@ -71,6 +71,8 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
}, out) }, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
t.Cleanup(func() { t.Cleanup(func() {
stop() stop()
@@ -86,8 +88,6 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
out.line(t, "msg", "stopped") out.line(t, "msg", "stopped")
}) })
addr, _ := out.line(t, "msg", "starting")["address"].(string)
return "http://" + addr, out return "http://" + addr, out
} }
@@ -105,7 +105,7 @@ func startWAF(t *testing.T, env map[string]string) (string, *output, func()) {
exited <- run(ctx, env, out) exited <- run(ctx, env, out)
}() }()
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
return addr, out, func() { return addr, out, func() {
stop() stop()
@@ -507,9 +507,10 @@ func TestAStatusWAFDoesNotTakeIsReportedAs599(t *testing.T) {
} }
// startRProxyProcess runs `sww rproxy` in a process of its own, with the // startRProxyProcess runs `sww rproxy` in a process of its own, with the
// settings in env, each as NAME=value. It returns where rproxy listens, the // settings in env, each as NAME=value. It returns where rproxy listens and a
// process, and what rproxy writes. // function that sends rproxy SIGTERM and fails the test unless it exits with
func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output) { // status 0 within waitLimit.
func startRProxyProcess(t *testing.T, env ...string) (string, func()) {
t.Helper() t.Helper()
binary, err := os.Executable() binary, err := os.Executable()
@@ -517,11 +518,8 @@ func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output
t.Fatalf("find the test binary: %v", err) t.Fatalf("find the test binary: %v", err)
} }
ctx, cancel := context.WithTimeout(t.Context(), 3*waitLimit)
t.Cleanup(cancel)
out := &output{} out := &output{}
cmd := exec.CommandContext(ctx, binary) //nolint:gosec // this test binary cmd := exec.CommandContext(t.Context(), binary) //nolint:gosec // this test binary
cmd.Stdout = out cmd.Stdout = out
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
@@ -529,19 +527,25 @@ func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output
"SWRP_LISTEN_ADDR="+localhost+":0") "SWRP_LISTEN_ADDR="+localhost+":0")
cmd.Env = append(cmd.Env, env...) cmd.Env = append(cmd.Env, env...)
err = cmd.Start() exited := startProcess(t, cmd)
if err != nil {
t.Fatalf("start: %v", err) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
return addr, func() {
err := cmd.Process.Signal(syscall.SIGTERM)
if err != nil {
t.Fatalf("signal: %v", err)
}
select {
case status := <-exited:
if status != 0 {
t.Errorf("exit status %d, want 0; output:\n%s", status, out.text())
}
case <-time.After(waitLimit):
t.Error("still running after being told to stop")
}
} }
t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
addr, _ := out.line(t, "msg", "starting")["address"].(string)
return addr, cmd, out
} }
func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) { func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
@@ -572,7 +576,7 @@ func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
t.Fatalf("write the certificate: %v", err) t.Fatalf("write the certificate: %v", err)
} }
addr, _, _ := startRProxyProcess(t, addr, _ := startRProxyProcess(t,
wafBackend+"="+backend.URL, wafBackend+"="+backend.URL,
"SWRP_UPSTREAM_URL="+startApp(t), "SWRP_UPSTREAM_URL="+startApp(t),
"SSL_CERT_FILE="+certificates) "SSL_CERT_FILE="+certificates)
@@ -651,7 +655,7 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
exited <- runRProxy(ctx, env, out) exited <- runRProxy(ctx, env, out)
}() }()
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
wantStatus(t, "http://"+addr+"/", placed, http.StatusOK) wantStatus(t, "http://"+addr+"/", placed, http.StatusOK)
@@ -750,7 +754,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
exited <- runRProxy(ctx, env, out) exited <- runRProxy(ctx, env, out)
}() }()
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
conn := sendAndKeepOpen(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n") conn := sendAndKeepOpen(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
select { select {
@@ -845,7 +849,7 @@ func TestMainRunsRProxy(t *testing.T) {
t.Parallel() t.Parallel()
// Nothing listens there; the health check does not ask. // Nothing listens there; the health check does not ask.
addr, cmd, out := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9") addr, stop := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9")
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
"http://"+addr+waf.HealthPath, http.NoBody) "http://"+addr+waf.HealthPath, http.NoBody)
@@ -868,13 +872,5 @@ func TestMainRunsRProxy(t *testing.T) {
t.Errorf("health check answered %q (%v)", body, err) t.Errorf("health check answered %q (%v)", body, err)
} }
err = cmd.Process.Signal(syscall.SIGTERM) stop()
if err != nil {
t.Fatalf("signal: %v", err)
}
err = cmd.Wait()
if err != nil {
t.Errorf("exit: %v; output:\n%s", err, out.text())
}
} }
+40 -18
View File
@@ -38,6 +38,35 @@ func TestMain(m *testing.M) {
os.Exit(sww.Main(testVersion)) os.Exit(sww.Main(testVersion))
} }
// startProcess starts cmd and returns a channel that gets its exit status. As
// the test ends, cmd is killed and the test waits for it to exit.
func startProcess(t *testing.T, cmd *exec.Cmd) <-chan int {
t.Helper()
err := cmd.Start()
if err != nil {
t.Fatalf("start: %v", err)
}
exited := make(chan int, 1)
waitReturned := make(chan struct{})
go func() {
_ = cmd.Wait()
exited <- cmd.ProcessState.ExitCode()
close(waitReturned)
}()
t.Cleanup(func() {
_ = cmd.Process.Kill()
<-waitReturned
})
return exited
}
func TestMainRunsWAF(t *testing.T) { func TestMainRunsWAF(t *testing.T) {
t.Parallel() t.Parallel()
@@ -54,12 +83,9 @@ func TestMainRunsWAF(t *testing.T) {
t.Fatalf("find the test binary: %v", err) t.Fatalf("find the test binary: %v", err)
} }
ctx, cancel := context.WithTimeout(t.Context(), 3*waitLimit)
defer cancel()
dir := t.TempDir() dir := t.TempDir()
out := &output{} out := &output{}
cmd := exec.CommandContext(ctx, binary) //nolint:gosec // this test binary cmd := exec.CommandContext(t.Context(), binary) //nolint:gosec // this test binary
cmd.Stdout = out cmd.Stdout = out
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
@@ -70,17 +96,9 @@ func TestMainRunsWAF(t *testing.T) {
rulesDir+"="+t.TempDir(), rulesDir+"="+t.TempDir(),
lookupSource+"=off") lookupSource+"=off")
err = cmd.Start() exited := startProcess(t, cmd)
if err != nil {
t.Fatalf("start: %v", err)
}
t.Cleanup(func() { starting := out.lineBeforeExit(t, "starting", exited)
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
starting := out.line(t, "msg", "starting")
addr, _ := starting["address"].(string) addr, _ := starting["address"].(string)
ask(t, addr, "/", http.StatusOK, waf.DispositionAllow) ask(t, addr, "/", http.StatusOK, waf.DispositionAllow)
@@ -90,9 +108,13 @@ func TestMainRunsWAF(t *testing.T) {
t.Fatalf("signal: %v", err) t.Fatalf("signal: %v", err)
} }
err = cmd.Wait() select {
if err != nil { case status := <-exited:
t.Fatalf("exit: %v; output:\n%s", err, out.text()) if status != 0 {
t.Fatalf("exit status %d, want 0; output:\n%s", status, out.text())
}
case <-time.After(waitLimit):
t.Fatal("still running after being told to stop")
} }
wantUTC(t, "the process line", starting["time"]) wantUTC(t, "the process line", starting["time"])
@@ -215,7 +237,7 @@ func runWAF(t *testing.T, env map[string]string, use func(addr string)) {
exited <- run(ctx, env, out) exited <- run(ctx, env, out)
}() }()
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
use(addr) use(addr)
stop() stop()