fetch: strip all origin/chain headers so nothing leaks to targets
The fetch service only stripped the four X-Weircon-* control headers, so any forwarding header injected upstream (X-Forwarded-For, X-Real-IP, Via, CDN client-IP headers, …) passed straight through to the target — leaking the caller's IP and proxy chain. - Replace stripWeircon with stripIdentifying: removes the control headers plus all standard forwarding/origin-IP headers, with a prefix sweep for any vendor-specific X-Forwarded-* variant. - NPM advanced.conf clears the same headers (defense in depth). - Add TestStripIdentifying covering removal + survival of legit headers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
012a279955
commit
5c1da02984
@@ -21,6 +21,18 @@ if ($weircon_auth_ok = 0) {
|
|||||||
# 3. Strip the auth header before forwarding — backend should never see it.
|
# 3. Strip the auth header before forwarding — backend should never see it.
|
||||||
proxy_set_header X-Weircon-Random-Ip "";
|
proxy_set_header X-Weircon-Random-Ip "";
|
||||||
|
|
||||||
|
# 3b. Defense in depth: never forward origin/chain headers. The fetch service
|
||||||
|
# also strips these, but clearing them here means they never even reach it.
|
||||||
|
# In nginx, an empty value removes the header entirely.
|
||||||
|
proxy_set_header X-Forwarded-For "";
|
||||||
|
proxy_set_header X-Forwarded-Host "";
|
||||||
|
proxy_set_header X-Forwarded-Proto "";
|
||||||
|
proxy_set_header X-Forwarded-Scheme "";
|
||||||
|
proxy_set_header X-Forwarded-Port "";
|
||||||
|
proxy_set_header X-Real-IP "";
|
||||||
|
proxy_set_header Forwarded "";
|
||||||
|
proxy_set_header Via "";
|
||||||
|
|
||||||
# 4. Generous timeouts: upstream fetches can be slow.
|
# 4. Generous timeouts: upstream fetches can be slow.
|
||||||
proxy_connect_timeout 15s;
|
proxy_connect_timeout 15s;
|
||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 60s;
|
||||||
|
|||||||
+38
-4
@@ -145,17 +145,51 @@ func (p *poolTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
return p.pool[rc.proxyID].RoundTrip(req)
|
return p.pool[rc.proxyID].RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var weirconHeaders = []string{
|
// stripHeaders are removed from the outbound request so nothing about the
|
||||||
|
// caller or the proxy chain can reach the target. It covers our own control
|
||||||
|
// headers plus every standard forwarding / client-IP header that a fronting
|
||||||
|
// reverse proxy (NPM, nginx, Cloudflare, …) might inject. Any other
|
||||||
|
// "X-Forwarded-*" header is swept by prefix in stripIdentifying.
|
||||||
|
var stripHeaders = []string{
|
||||||
|
// weircon control headers
|
||||||
"X-Weircon-Random-Ip",
|
"X-Weircon-Random-Ip",
|
||||||
"X-Weircon-Random-Ip-Redirect",
|
"X-Weircon-Random-Ip-Redirect",
|
||||||
"X-Weircon-Proxy-Id",
|
"X-Weircon-Proxy-Id",
|
||||||
"X-Weircon-Forward-Method",
|
"X-Weircon-Forward-Method",
|
||||||
|
// generic forwarding / origin-IP headers
|
||||||
|
"Forwarded",
|
||||||
|
"Via",
|
||||||
|
"X-Real-Ip",
|
||||||
|
"X-Original-Forwarded-For",
|
||||||
|
"X-Client-Ip",
|
||||||
|
"X-Cluster-Client-Ip",
|
||||||
|
"X-Original-Url",
|
||||||
|
"X-Original-Host",
|
||||||
|
"X-Rewrite-Url",
|
||||||
|
"X-Proxy-Id",
|
||||||
|
// CDN / vendor client-IP headers
|
||||||
|
"Cf-Connecting-Ip",
|
||||||
|
"Cf-Ipcountry",
|
||||||
|
"Cf-Ray",
|
||||||
|
"True-Client-Ip",
|
||||||
|
"Fastly-Client-Ip",
|
||||||
|
"Fly-Client-Ip",
|
||||||
|
"X-Appengine-User-Ip",
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripWeircon(h http.Header) {
|
// stripIdentifying removes every header that could reveal the caller or the
|
||||||
for _, k := range weirconHeaders {
|
// proxy chain. The explicit list catches the common ones; the prefix sweep
|
||||||
|
// catches any vendor-specific X-Forwarded-* we didn't enumerate. Header keys
|
||||||
|
// in an http.Header are already canonicalized, so prefix matching is exact.
|
||||||
|
func stripIdentifying(h http.Header) {
|
||||||
|
for _, k := range stripHeaders {
|
||||||
h.Del(k)
|
h.Del(k)
|
||||||
}
|
}
|
||||||
|
for k := range h {
|
||||||
|
if strings.HasPrefix(k, "X-Forwarded") {
|
||||||
|
h.Del(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pickProxyID(req *http.Request, n int) (int, error) {
|
func pickProxyID(req *http.Request, n int) (int, error) {
|
||||||
@@ -212,7 +246,7 @@ func newHandler(cfg Config, pool []*http.Transport) http.Handler {
|
|||||||
pr.Out.Host = rc.target.Host
|
pr.Out.Host = rc.target.Host
|
||||||
pr.Out.Method = rc.method
|
pr.Out.Method = rc.method
|
||||||
pr.Out.RequestURI = ""
|
pr.Out.RequestURI = ""
|
||||||
stripWeircon(pr.Out.Header)
|
stripIdentifying(pr.Out.Header)
|
||||||
},
|
},
|
||||||
Transport: &poolTransport{pool: pool},
|
Transport: &poolTransport{pool: pool},
|
||||||
ModifyResponse: func(resp *http.Response) error {
|
ModifyResponse: func(resp *http.Response) error {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStripIdentifying(t *testing.T) {
|
||||||
|
h := http.Header{}
|
||||||
|
|
||||||
|
// Headers that MUST be removed before the request reaches the target.
|
||||||
|
removed := []string{
|
||||||
|
"X-Weircon-Random-Ip",
|
||||||
|
"X-Weircon-Proxy-Id",
|
||||||
|
"X-Forwarded-For",
|
||||||
|
"X-Forwarded-Host",
|
||||||
|
"X-Forwarded-Proto",
|
||||||
|
"X-Forwarded-Port",
|
||||||
|
"X-Forwarded-Custom-Vendor", // caught by prefix sweep
|
||||||
|
"X-Real-Ip",
|
||||||
|
"Forwarded",
|
||||||
|
"Via",
|
||||||
|
"Cf-Connecting-Ip",
|
||||||
|
"True-Client-Ip",
|
||||||
|
"Fastly-Client-Ip",
|
||||||
|
"X-Original-Forwarded-For",
|
||||||
|
"X-Client-Ip",
|
||||||
|
}
|
||||||
|
for _, k := range removed {
|
||||||
|
h.Set(k, "leak")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers a crawler legitimately sets — these MUST survive untouched.
|
||||||
|
kept := map[string]string{
|
||||||
|
"User-Agent": "my-crawler/1.0",
|
||||||
|
"Cookie": "session=abc",
|
||||||
|
"Accept": "text/html",
|
||||||
|
"Accept-Language": "en-US",
|
||||||
|
"Referer": "https://example.com",
|
||||||
|
"X-Forward": "not-a-forwarding-header", // does not match "X-Forwarded" prefix
|
||||||
|
}
|
||||||
|
for k, v := range kept {
|
||||||
|
h.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
stripIdentifying(h)
|
||||||
|
|
||||||
|
for _, k := range removed {
|
||||||
|
if got := h.Get(k); got != "" {
|
||||||
|
t.Errorf("expected %q to be stripped, still present: %q", k, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, want := range kept {
|
||||||
|
if got := h.Get(k); got != want {
|
||||||
|
t.Errorf("expected %q to survive as %q, got %q", k, want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user