git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge commit from fork

Signed-off-by: Jan Larwig <jan@larwig.com>

commitaff369dfa31ca6b8166b4e14613e8fb2d7dac88a authorJan Larwig <jan@larwig.com> dateMon 13 Apr 2026, 18:22 (6 months ago) parent43596a7
23 files changed+498−104
CHANGELOG.md+1 contrib/local-environment/docker-compose-nginx.yaml+3−2 contrib/local-environment/nginx.conf+6−48 contrib/local-environment/oauth2-proxy-nginx.cfg+11−6 docs/docs/configuration/overview.md+5 docs/versioned_docs/version-7.15.x/configuration/overview.md+5 oauthproxy.go+23−10 oauthproxy_test.go+38 pkg/apis/middleware/scope.go+48−1 pkg/apis/middleware/scope_test.go+34 pkg/apis/options/options.go+2 pkg/app/redirect/director_test.go+33−2 pkg/cookies/cookies_test.go+7−1 pkg/ip/parse_ip_net.go+16 pkg/ip/parse_ip_net_test.go+118 pkg/middleware/redirect_to_https_test.go+15 pkg/middleware/scope.go+5−3 pkg/middleware/scope_test.go+18−4 pkg/requests/util/util.go+14−13 pkg/requests/util/util_test.go+52−12 pkg/upstream/http_test.go+2−2 pkg/validation/allowlist.go+12 pkg/validation/allowlist_test.go+30
CHANGELOG.md+1
@@ -12,6 +12,7 @@
1212- [#3333](https://github.com/oauth2-proxy/oauth2-proxy/pull/3333) fix: invalidate session on fatal OAuth2 refresh errors (@frhack)
1313- [GHSA-f24x-5g9q-753f](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-f24x-5g9q-753f) fix: clear session cookie at beginning of signinpage handler (@fnoehWM / @bella-WI / @tuunit)
1414- [GHSA-5hvv-m4w4-gf6v](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5hvv-m4w4-gf6v) fix: health check user-agent authentication bypass (@tuunit)
15+- [GHSA-7x63-xv5r-3p2x](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-7x63-xv5r-3p2x) fix: authentication bypass via X-Forwarded-Uri header spoofing (@tuunit)
1516
1617# V7.15.1
1718
contrib/local-environment/docker-compose-nginx.yaml+3−2
@@ -23,7 +23,8 @@ version: "3.0"
2323services:
2424 oauth2-proxy:
2525 image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
26− ports: []
26+ ports:
27+ - 4180:4180/tcp
2728 hostname: oauth2-proxy
2829 container_name: oauth2-proxy
2930 command: --config /oauth2-proxy.cfg
@@ -44,7 +45,7 @@ services:
4445 image: nginx:1.29
4546 restart: unless-stopped
4647 ports:
47− - 80:80/tcp
48+ - 8080:8080/tcp
4849 hostname: nginx
4950 volumes:
5051 - "./nginx.conf:/etc/nginx/conf.d/default.conf"
contrib/local-environment/nginx.conf+6−48
@@ -1,11 +1,12 @@
11# Reverse proxy to oauth2-proxy
22server {
3− listen 80;
4− server_name oauth2-proxy.oauth2-proxy.localhost;
3+ listen 8080;
4+ server_name oauth2-proxy.localtest.me;
55
66 location / {
77 proxy_set_header Host $host;
88 proxy_set_header X-Real-IP $remote_addr;
9+ proxy_set_header X-Forwarded-Uri $request_uri;
910
1011 proxy_pass http://oauth2-proxy:4180/;
1112 }
@@ -13,8 +14,8 @@ server {
1314
1415# Reverse proxy to httpbin
1516server {
16− listen 80;
17− server_name httpbin.oauth2-proxy.localhost;
17+ listen 8080;
18+ server_name httpbin.localtest.me;
1819
1920 auth_request /internal-auth/oauth2/auth;
2021
@@ -29,50 +30,7 @@ server {
2930 # Named location for OAuth2 sign-in redirect
3031 # Returns a proper 302 that works with --skip-provider-button
3132 location @oauth2_signin {
32− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
33− }
34−
35− # auth_request must be a URI so this allows an internal path to then proxy to
36− # the real auth_request path.
37− # The trailing /'s are required so that nginx strips the prefix before proxying.
38− location /internal-auth/ {
39− internal; # Ensure external users can't access this path
40−
41− # Make sure the OAuth2 Proxy knows where the original request came from.
42− proxy_set_header Host $host;
43− proxy_set_header X-Real-IP $remote_addr;
44− proxy_set_header X-Forwarded-Uri $request_uri;
45−
46− proxy_pass http://oauth2-proxy:4180/;
47− }
48−}
49−
50−# Statically serve the nginx welcome
51−server {
52− listen 80;
53− server_name oauth2-proxy.localhost;
54−
55− location / {
56− auth_request /internal-auth/oauth2/auth;
57−
58− # On 401, redirect to the sign_in page via a named location
59− # This ensures a proper 302 redirect that browsers will follow
60− error_page 401 = @oauth2_signin;
61−
62− root /usr/share/nginx/html;
63− index index.html index.htm;
64− }
65−
66− # Named location for OAuth2 sign-in redirect
67− # Returns a proper 302 that works with --skip-provider-button
68− location @oauth2_signin {
69− return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
70− }
71−
72− # redirect server error pages to the static page /50x.html
73− error_page 500 502 503 504 /50x.html;
74− location = /50x.html {
75− root /usr/share/nginx/html;
33+ return 302 http://oauth2-proxy.localtest.me:8080/oauth2/sign_in?rd=$scheme://$host$request_uri;
7634 }
7735
7836 # auth_request must be a URI so this allows an internal path to then proxy to
contrib/local-environment/oauth2-proxy-nginx.cfg+11−6
@@ -1,14 +1,19 @@
11http_address="0.0.0.0:4180"
22cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3−provider="oidc"
43email_domains="example.com"
5−oidc_issuer_url="http://dex.localtest.me:5556/dex"
4+cookie_secure="false"
5+upstreams="static://200"
6+cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains.
7+whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target.
8+
9+# dex provider
610client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
711client_id="oauth2-proxy"
8−cookie_secure="false"
12+redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback"
13+
14+oidc_issuer_url="http://dex.localtest.me:5556/dex"
15+provider="oidc"
16+provider_display_name="Dex"
917
10−redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
11−cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
12−whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
1318# Enables the use of `X-Forwarded-*` headers to determine request correctly
1419reverse_proxy="true"
docs/docs/configuration/overview.md+5
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
193193
194194### Proxy Options
195195
196+:::warning
197+When `--reverse-proxy` is enabled, configure `--trusted-proxy-ip` to the IPs or CIDR ranges of the reverse proxies that are allowed to send `X-Forwarded-*` headers. If you leave it unset, OAuth2 Proxy currently trusts all source IPs for backwards compatibility, which means a client that can reach OAuth2 Proxy directly may be able to spoof forwarded headers.
198+:::
199+
196200| Flag / Config Field | Type | Description | Default |
197201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
198202| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
@@ -211,6 +215,7 @@ Provider specific options can be found on their respective subpages.
211215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
212216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
213217| flag: `--reverse-proxy`<br/>toml: `reverse_proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted and allows X-Forwarded-\{Proto,Host,Uri\} headers to be used on redirect selection | false |
218+| flag: `--trusted-proxy-ip`<br/>toml: `trusted_proxy_ips` | string \| list | list of IPs or CIDR ranges allowed to supply `X-Forwarded-*` headers when `--reverse-proxy` is enabled. If not set, OAuth2 Proxy preserves backwards compatibility by trusting all source IPs (`0.0.0.0/0`, `::/0`) and logs a warning at startup. Configure this to your reverse proxy addresses to prevent forwarded header spoofing. | `"0.0.0.0/0", "::/0"` |
214219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
215220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
216221| flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
docs/versioned_docs/version-7.15.x/configuration/overview.md+5
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
193193
194194### Proxy Options
195195
196+:::warning
197+When `--reverse-proxy` is enabled, configure `--trusted-proxy-ip` to the IPs or CIDR ranges of the reverse proxies that are allowed to send `X-Forwarded-*` headers. If you leave it unset, OAuth2 Proxy currently trusts all source IPs for backwards compatibility, which means a client that can reach OAuth2 Proxy directly may be able to spoof forwarded headers.
198+:::
199+
196200| Flag / Config Field | Type | Description | Default |
197201| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
198202| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
@@ -211,6 +215,7 @@ Provider specific options can be found on their respective subpages.
211215| flag: `--redirect-url`<br/>toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
212216| flag: `--relative-redirect-url`<br/>toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
213217| flag: `--reverse-proxy`<br/>toml: `reverse_proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-IP are accepted and allows X-Forwarded-\{Proto,Host,Uri\} headers to be used on redirect selection | false |
218+| flag: `--trusted-proxy-ip`<br/>toml: `trusted_proxy_ips` | string \| list | list of IPs or CIDR ranges allowed to supply `X-Forwarded-*` headers when `--reverse-proxy` is enabled. If not set, OAuth2 Proxy preserves backwards compatibility by trusting all source IPs (`0.0.0.0/0`, `::/0`) and logs a warning at startup. Configure this to your reverse proxy addresses to prevent forwarded header spoofing. | `"0.0.0.0/0", "::/0"` |
214219| flag: `--signature-key`<br/>toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
215220| flag: `--skip-auth-preflight`<br/>toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
216221| flag: `--skip-auth-regex`<br/>toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
oauthproxy.go+23−10
@@ -59,6 +59,8 @@ const (
5959)
6060
6161var (
62+ defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
63+
6264 // ErrNeedsLogin means the user should be redirected to the login page
6365 ErrNeedsLogin = errors.New("redirect to login page")
6466
@@ -183,13 +185,14 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
183185
184186 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
185187
186− trustedIPs := ip.NewNetSet()
187− for _, ipStr := range opts.TrustedIPs {
188− if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
189− trustedIPs.AddIPNet(*ipNet)
190− } else {
191− return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
192− }
188+ trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
189+ if err != nil {
190+ return nil, err
191+ }
192+
193+ trustedProxies, err := buildTrustedProxyNetSet(opts)
194+ if err != nil {
195+ return nil, err
193196 }
194197
195198 allowedRoutes, err := buildRoutesAllowlist(opts)
@@ -202,7 +205,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
202205 return nil, err
203206 }
204207
205− preAuthChain, err := buildPreAuthChain(opts, sessionStore)
208+ preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
206209 if err != nil {
207210 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
208211 }
@@ -355,8 +358,8 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
355358// buildPreAuthChain constructs a chain that should process every request before
356359// the OAuth2 Proxy authentication logic kicks in.
357360// For example forcing HTTPS or health checks.
358−func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
359− chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
361+func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
362+ chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
360363
361364 if opts.ForceHTTPS {
362365 _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
@@ -395,6 +398,16 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt
395398 return chain, nil
396399}
397400
401+func buildTrustedProxyNetSet(opts *options.Options) (*ip.NetSet, error) {
402+ trustedProxyIPs := opts.TrustedProxyIPs
403+ if opts.ReverseProxy && len(trustedProxyIPs) == 0 {
404+ logger.Print("WARNING: --reverse-proxy is enabled but no --trusted-proxy-ip CIDRs were configured. All connecting IPs are trusted to supply X-Forwarded-* headers by default (0.0.0.0/0, ::/0). This preserves backwards compatibility but is a potential security risk; configure --trusted-proxy-ip to match your reverse proxy addresses.")
405+ trustedProxyIPs = defaultTrustedProxyIPs
406+ }
407+
408+ return ip.ParseNetSet(trustedProxyIPs)
409+}
410+
398411func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
399412 chain := alice.New()
400413
oauthproxy_test.go+38
@@ -2843,6 +2843,7 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
28432843 t.Run(tc.name, func(t *testing.T) {
28442844 req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
28452845 req.Header.Set("X-Forwarded-Uri", tc.url)
2846+ req.RemoteAddr = "127.0.0.1:4180"
28462847 assert.NoError(t, err)
28472848
28482849 rw := httptest.NewRecorder()
@@ -2857,6 +2858,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
28572858 }
28582859}
28592860
2861+func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
2862+ upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2863+ w.WriteHeader(200)
2864+ }))
2865+ t.Cleanup(upstreamServer.Close)
2866+
2867+ opts := baseTestOptions()
2868+ opts.ReverseProxy = true
2869+ opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
2870+ opts.UpstreamServers = options.UpstreamConfig{
2871+ Upstreams: []options.Upstream{
2872+ {
2873+ ID: upstreamServer.URL,
2874+ Path: "/",
2875+ URI: upstreamServer.URL,
2876+ },
2877+ },
2878+ }
2879+ opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
2880+
2881+ err := validation.Validate(opts)
2882+ assert.NoError(t, err)
2883+
2884+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
2885+ assert.NoError(t, err)
2886+
2887+ req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
2888+ assert.NoError(t, err)
2889+ req.RemoteAddr = "192.0.2.10:4180"
2890+ req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
2891+
2892+ rw := httptest.NewRecorder()
2893+ proxy.ServeHTTP(rw, req)
2894+
2895+ assert.Equal(t, 401, rw.Code)
2896+}
2897+
28602898func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
28612899 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28622900 w.WriteHeader(200)
pkg/apis/middleware/scope.go+48−1
@@ -2,9 +2,12 @@ package middleware
22
33import (
44 "context"
5+ "net"
56 "net/http"
7+ "strings"
68
79 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
811)
912
1013type scopeKey string
@@ -18,9 +21,13 @@ const RequestScopeKey scopeKey = "request-scope"
1821// within the chain.
1922type RequestScope struct {
2023 // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
21− // mode and if request `X-Forwarded-*` headers should be trusted
24+ // mode and if request `X-Forwarded-*` headers may be trusted
2225 ReverseProxy bool
2326
27+ // TrustedProxies tracks which direct callers are allowed to supply
28+ // forwarded headers when ReverseProxy mode is enabled.
29+ TrustedProxies *ip.NetSet
30+
2431 // RequestID is set to the request's `X-Request-Id` header if set.
2532 // Otherwise a random UUID is set.
2633 RequestID string
@@ -58,3 +65,43 @@ func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
5865 ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
5966 return req.WithContext(ctx)
6067}
68+
69+// CanTrustForwardedHeaders returns whether forwarded headers should be
70+// processed for this request.
71+func (s *RequestScope) CanTrustForwardedHeaders(req *http.Request) bool {
72+ if s == nil || req == nil || !s.ReverseProxy || s.TrustedProxies == nil {
73+ return false
74+ }
75+
76+ if isUnixSocketRemoteAddr(req.RemoteAddr) {
77+ return true
78+ }
79+
80+ remoteIP := parseRemoteAddrIP(req.RemoteAddr)
81+ if remoteIP == nil {
82+ return false
83+ }
84+
85+ return s.TrustedProxies.Has(remoteIP)
86+}
87+
88+func parseRemoteAddrIP(remoteAddr string) net.IP {
89+ if remoteAddr == "" {
90+ return nil
91+ }
92+
93+ if ip := net.ParseIP(remoteAddr); ip != nil {
94+ return ip
95+ }
96+
97+ host, _, err := net.SplitHostPort(remoteAddr)
98+ if err != nil {
99+ return nil
100+ }
101+
102+ return net.ParseIP(host)
103+}
104+
105+func isUnixSocketRemoteAddr(remoteAddr string) bool {
106+ return remoteAddr == "@" || strings.HasPrefix(remoteAddr, "/")
107+}
pkg/apis/middleware/scope_test.go+34
@@ -4,6 +4,7 @@ import (
44 "net/http"
55
66 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
78 . "github.com/onsi/ginkgo/v2"
89 . "github.com/onsi/gomega"
910)
@@ -53,4 +54,37 @@ var _ = Describe("Scope Suite", func() {
5354 })
5455 })
5556 })
57+
58+ Context("CanTrustForwardedHeaders", func() {
59+ var request *http.Request
60+ var scope *middleware.RequestScope
61+
62+ BeforeEach(func() {
63+ var err error
64+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
65+ Expect(err).ToNot(HaveOccurred())
66+
67+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
68+ Expect(err).ToNot(HaveOccurred())
69+ scope = &middleware.RequestScope{
70+ ReverseProxy: true,
71+ TrustedProxies: trustedProxies,
72+ }
73+ })
74+
75+ It("returns true for a trusted remote address", func() {
76+ request.RemoteAddr = "127.0.0.1:4180"
77+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
78+ })
79+
80+ It("returns false for an untrusted remote address", func() {
81+ request.RemoteAddr = "192.0.2.10:4180"
82+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeFalse())
83+ })
84+
85+ It("returns true for unix socket callers", func() {
86+ request.RemoteAddr = "@"
87+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
88+ })
89+ })
5690})
pkg/apis/options/options.go+2
@@ -24,6 +24,7 @@ type Options struct {
2424 ReadyPath string `flag:"ready-path" cfg:"ready_path"`
2525 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
2626 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
27+ TrustedProxyIPs []string `flag:"trusted-proxy-ip" cfg:"trusted_proxy_ips"`
2728 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
2829 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
2930 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
@@ -119,6 +120,7 @@ func NewFlagSet() *pflag.FlagSet {
119120
120121 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
121122 flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, X-ProxyUser-IP, X-Envoy-External-Address, or CF-Connecting-IP)")
123+ flagSet.StringSlice("trusted-proxy-ip", []string{}, "list of IPs or CIDR ranges that are allowed to set X-Forwarded-* headers when --reverse-proxy is enabled. Defaults to trusting all IPs for backwards compatibility; configure this to your reverse proxy addresses to prevent header spoofing.")
122124 flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.")
123125 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
124126 flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
pkg/app/redirect/director_test.go+33−2
@@ -4,6 +4,7 @@ import (
44 "net/http"
55
66 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
78 . "github.com/onsi/ginkgo/v2"
89 . "github.com/onsi/gomega"
910)
@@ -33,9 +34,16 @@ var _ = Describe("Director Suite", func() {
3334 req.Header.Add(header, value)
3435 }
3536 }
36− req = middleware.AddRequestScope(req, &middleware.RequestScope{
37+ scope := &middleware.RequestScope{
3738 ReverseProxy: in.reverseProxy,
38− })
39+ }
40+ if in.reverseProxy {
41+ req.RemoteAddr = "127.0.0.1:4180"
42+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
43+ Expect(err).ToNot(HaveOccurred())
44+ scope.TrustedProxies = trustedProxies
45+ }
46+ req = middleware.AddRequestScope(req, scope)
3947
4048 redirect, err := appDirector.GetRedirect(req)
4149 Expect(err).ToNot(HaveOccurred())
@@ -174,4 +182,27 @@ var _ = Describe("Director Suite", func() {
174182 expectedRedirect: "https://a-service.example.com/foo/bar",
175183 }),
176184 )
185+
186+ It("ignores forwarded headers from an untrusted remote address", func() {
187+ appDirector := NewAppDirector(AppDirectorOpts{
188+ ProxyPrefix: testProxyPrefix,
189+ Validator: testValidator(true),
190+ })
191+
192+ req, _ := http.NewRequest("GET", "https://oauth.example.com/foo?bar", nil)
193+ req.RemoteAddr = "192.0.2.10:4180"
194+ req.Header.Add("X-Forwarded-Proto", "https")
195+ req.Header.Add("X-Forwarded-Host", "a-service.example.com")
196+ req.Header.Add("X-Forwarded-Uri", fooBar)
197+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
198+ Expect(err).ToNot(HaveOccurred())
199+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
200+ ReverseProxy: true,
201+ TrustedProxies: trustedProxies,
202+ })
203+
204+ redirect, err := appDirector.GetRedirect(req)
205+ Expect(err).ToNot(HaveOccurred())
206+ Expect(redirect).To(Equal("/foo?bar"))
207+ })
177208})
pkg/cookies/cookies_test.go+7−1
@@ -6,6 +6,7 @@ import (
66 "time"
77
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -30,8 +31,13 @@ var _ = Describe("Cookie Tests", func() {
3031
3132 if in.xForwardedHost != "" {
3233 req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
34+ req.RemoteAddr = "127.0.0.1:4180"
35+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
36+ Expect(err).ToNot(HaveOccurred())
37+
3338 req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
34− ReverseProxy: true,
39+ ReverseProxy: true,
40+ TrustedProxies: trustedProxies,
3541 })
3642 }
3743
pkg/ip/parse_ip_net.go+16
@@ -1,6 +1,7 @@
11package ip
22
33import (
4+ "fmt"
45 "net"
56 "strings"
67)
@@ -37,3 +38,18 @@ func ParseIPNet(s string) *net.IPNet {
3738 return ipNet
3839 }
3940}
41+
42+func ParseNetSet(ipStrs []string) (*NetSet, error) {
43+ netSet := NewNetSet()
44+
45+ for _, ipStr := range ipStrs {
46+ ipNet := ParseIPNet(ipStr)
47+ if ipNet == nil {
48+ return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
49+ }
50+
51+ netSet.AddIPNet(*ipNet)
52+ }
53+
54+ return netSet, nil
55+}
pkg/ip/parse_ip_net_test.gonew file+118
@@ -0,0 +1,118 @@
1+package ip
2+
3+import (
4+ "net"
5+ "testing"
6+
7+ "github.com/stretchr/testify/assert"
8+)
9+
10+func TestParseIPNet(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ input string
14+ expectedIP net.IP
15+ expectedMask net.IPMask
16+ }{
17+ {
18+ name: "ipv4 address",
19+ input: "127.0.0.1",
20+ expectedIP: net.ParseIP("127.0.0.1"),
21+ expectedMask: net.CIDRMask(32, 32),
22+ },
23+ {
24+ name: "ipv6 address",
25+ input: "::1",
26+ expectedIP: net.ParseIP("::1"),
27+ expectedMask: net.CIDRMask(128, 128),
28+ },
29+ {
30+ name: "ipv4 cidr",
31+ input: "10.0.0.0/24",
32+ expectedIP: net.ParseIP("10.0.0.0"),
33+ expectedMask: net.CIDRMask(24, 32),
34+ },
35+ {
36+ name: "ipv6 cidr",
37+ input: "2001:db8::/64",
38+ expectedIP: net.ParseIP("2001:db8::"),
39+ expectedMask: net.CIDRMask(64, 128),
40+ },
41+ }
42+
43+ for _, test := range tests {
44+ t.Run(test.name, func(t *testing.T) {
45+ ipNet := ParseIPNet(test.input)
46+
47+ assert.NotNil(t, ipNet)
48+ if ipNet == nil {
49+ return
50+ }
51+
52+ assert.True(t, test.expectedIP.Equal(ipNet.IP))
53+ assert.Equal(t, test.expectedMask, ipNet.Mask)
54+ })
55+ }
56+}
57+
58+func TestParseIPNetRejectsInvalidNetworks(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ input string
62+ }{
63+ {
64+ name: "invalid ip",
65+ input: "not-an-ip",
66+ },
67+ {
68+ name: "ipv4 cidr with host bits set",
69+ input: "10.0.0.1/24",
70+ },
71+ {
72+ name: "ipv6 cidr with host bits set",
73+ input: "2001:db8::1/64",
74+ },
75+ {
76+ name: "invalid cidr mask",
77+ input: "10.0.0.0/33",
78+ },
79+ }
80+
81+ for _, test := range tests {
82+ t.Run(test.name, func(t *testing.T) {
83+ assert.Nil(t, ParseIPNet(test.input))
84+ })
85+ }
86+}
87+
88+func TestParseNetSet(t *testing.T) {
89+ netSet, err := ParseNetSet([]string{
90+ "127.0.0.1",
91+ "10.0.0.0/24",
92+ "::1",
93+ "2001:db8::/64",
94+ })
95+
96+ assert.NoError(t, err)
97+ assert.NotNil(t, netSet)
98+ if netSet == nil {
99+ return
100+ }
101+
102+ assert.True(t, netSet.Has(net.ParseIP("127.0.0.1")))
103+ assert.True(t, netSet.Has(net.ParseIP("10.0.0.55")))
104+ assert.True(t, netSet.Has(net.ParseIP("::1")))
105+ assert.True(t, netSet.Has(net.ParseIP("2001:db8::abcd")))
106+
107+ assert.False(t, netSet.Has(net.ParseIP("127.0.0.2")))
108+ assert.False(t, netSet.Has(net.ParseIP("10.0.1.1")))
109+ assert.False(t, netSet.Has(net.ParseIP("::2")))
110+ assert.False(t, netSet.Has(net.ParseIP("2001:db9::1")))
111+}
112+
113+func TestParseNetSetReturnsErrorForInvalidNetwork(t *testing.T) {
114+ netSet, err := ParseNetSet([]string{"127.0.0.1", "10.0.0.1/24"})
115+
116+ assert.Nil(t, netSet)
117+ assert.EqualError(t, err, "could not parse IP network (10.0.0.1/24)")
118+}
pkg/middleware/redirect_to_https_test.go+15
@@ -6,6 +6,7 @@ import (
66 "net/http/httptest"
77
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -39,6 +40,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
3940 scope := &middlewareapi.RequestScope{
4041 ReverseProxy: in.reverseProxy,
4142 }
43+ if in.reverseProxy {
44+ req.RemoteAddr = "127.0.0.1:4180"
45+ scope.TrustedProxies = newRedirectTrustedProxySet("127.0.0.1")
46+ }
4247 req = middlewareapi.AddRequestScope(req, scope)
4348
4449 rw := httptest.NewRecorder()
@@ -207,3 +212,13 @@ var _ = Describe("RedirectToHTTPS suite", func() {
207212 }),
208213 )
209214})
215+
216+func newRedirectTrustedProxySet(cidrs ...string) *ip.NetSet {
217+ set := ip.NewNetSet()
218+ for _, cidr := range cidrs {
219+ ipNet := ip.ParseIPNet(cidr)
220+ Expect(ipNet).ToNot(BeNil())
221+ set.AddIPNet(*ipNet)
222+ }
223+ return set
224+}
pkg/middleware/scope.go+5−3
@@ -6,14 +6,16 @@ import (
66 "github.com/google/uuid"
77 "github.com/justinas/alice"
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910)
1011
11−func NewScope(reverseProxy bool, idHeader string) alice.Constructor {
12+func NewScope(reverseProxy bool, idHeader string, trustedProxies *ip.NetSet) alice.Constructor {
1213 return func(next http.Handler) http.Handler {
1314 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1415 scope := &middlewareapi.RequestScope{
15− ReverseProxy: reverseProxy,
16− RequestID: genRequestID(req, idHeader),
16+ ReverseProxy: reverseProxy,
17+ TrustedProxies: trustedProxies,
18+ RequestID: genRequestID(req, idHeader),
1719 }
1820 req = middlewareapi.AddRequestScope(req, scope)
1921 next.ServeHTTP(rw, req)
pkg/middleware/scope_test.go+18−4
@@ -6,6 +6,7 @@ import (
66
77 "github.com/google/uuid"
88 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 . "github.com/onsi/ginkgo/v2"
1011 . "github.com/onsi/gomega"
1112)
@@ -32,7 +33,7 @@ var _ = Describe("Scope Suite", func() {
3233
3334 Context("ReverseProxy is false", func() {
3435 BeforeEach(func() {
35− handler := NewScope(false, testRequestHeader)(
36+ handler := NewScope(false, testRequestHeader, nil)(
3637 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3738 nextRequest = r
3839 w.WriteHeader(200)
@@ -60,8 +61,15 @@ var _ = Describe("Scope Suite", func() {
6061 })
6162
6263 Context("ReverseProxy is true", func() {
64+ var trustedProxies *ip.NetSet
65+
6366 BeforeEach(func() {
64− handler := NewScope(true, testRequestHeader)(
67+ var err error
68+
69+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
70+ Expect(err).ToNot(HaveOccurred())
71+
72+ handler := NewScope(true, testRequestHeader, trustedProxies)(
6573 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6674 nextRequest = r
6775 w.WriteHeader(200)
@@ -74,12 +82,18 @@ var _ = Describe("Scope Suite", func() {
7482 Expect(scope).ToNot(BeNil())
7583 Expect(scope.ReverseProxy).To(BeTrue())
7684 })
85+
86+ It("stores the trusted proxies on the scope", func() {
87+ scope := middlewareapi.GetRequestScope(nextRequest)
88+ Expect(scope).ToNot(BeNil())
89+ Expect(scope.TrustedProxies).To(BeIdenticalTo(trustedProxies))
90+ })
7791 })
7892
7993 Context("Request ID header is present", func() {
8094 BeforeEach(func() {
8195 request.Header.Add(testRequestHeader, testRequestID)
82− handler := NewScope(false, testRequestHeader)(
96+ handler := NewScope(false, testRequestHeader, nil)(
8397 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8498 nextRequest = r
8599 w.WriteHeader(200)
@@ -97,7 +111,7 @@ var _ = Describe("Scope Suite", func() {
97111 BeforeEach(func() {
98112 uuid.SetRand(mockRand{})
99113
100− handler := NewScope(true, testRequestHeader)(
114+ handler := NewScope(true, testRequestHeader, nil)(
101115 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102116 nextRequest = r
103117 w.WriteHeader(200)
pkg/requests/util/util.go+14−13
@@ -15,30 +15,30 @@ const (
1515)
1616
1717// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
18−// and the request is proxied.
18+// and the request came from a trusted reverse proxy.
1919func GetRequestProto(req *http.Request) string {
2020 proto := req.Header.Get(XForwardedProto)
21− if !IsProxied(req) || proto == "" {
21+ if !CanTrustForwardedHeaders(req) || proto == "" {
2222 proto = req.URL.Scheme
2323 }
2424 return proto
2525}
2626
2727// GetRequestHost returns the request host header or X-Forwarded-Host if
28−// present and the request is proxied.
28+// present and the request came from a trusted reverse proxy.
2929func GetRequestHost(req *http.Request) string {
3030 host := req.Header.Get(XForwardedHost)
31− if !IsProxied(req) || host == "" {
31+ if !CanTrustForwardedHeaders(req) || host == "" {
3232 host = req.Host
3333 }
3434 return host
3535}
3636
3737// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
38−// request is proxied.
38+// request came from a trusted reverse proxy.
3939func GetRequestURI(req *http.Request) string {
4040 uri := req.Header.Get(XForwardedURI)
41− if !IsProxied(req) || uri == "" {
41+ if !CanTrustForwardedHeaders(req) || uri == "" {
4242 // Use RequestURI to preserve ?query
4343 uri = req.URL.RequestURI()
4444 }
@@ -46,8 +46,8 @@ func GetRequestURI(req *http.Request) string {
4646}
4747
4848// GetRequestPath returns the request URI or X-Forwarded-Uri if present and the
49−// request is proxied but always strips the query parameters and only returns
50−// the pure path
49+// request came from a trusted reverse proxy but always strips the query
50+// parameters and only returns the pure path.
5151func GetRequestPath(req *http.Request) string {
5252 uri := GetRequestURI(req)
5353
@@ -64,17 +64,18 @@ func GetRequestPath(req *http.Request) string {
6464 return uri
6565}
6666
67−// IsProxied determines if a request was from a proxy based on the RequestScope
68−// ReverseProxy tracker.
69−func IsProxied(req *http.Request) bool {
67+// CanTrustForwardedHeaders determines if forwarded headers should be processed
68+// based on the RequestScope and the direct caller's address.
69+func CanTrustForwardedHeaders(req *http.Request) bool {
7070 scope := middlewareapi.GetRequestScope(req)
7171 if scope == nil {
7272 return false
7373 }
74− return scope.ReverseProxy
74+
75+ return scope.CanTrustForwardedHeaders(req)
7576}
7677
7778func IsForwardedRequest(req *http.Request) bool {
78− return IsProxied(req) &&
79+ return CanTrustForwardedHeaders(req) &&
7980 req.Host != GetRequestHost(req)
8081}
pkg/requests/util/util_test.go+52−12
@@ -6,6 +6,7 @@ import (
66 "net/http/httptest"
77
88 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1011 . "github.com/onsi/ginkgo/v2"
1112 . "github.com/onsi/gomega"
@@ -19,8 +20,13 @@ var _ = Describe("Util Suite", func() {
1920 uriNoQueryParams = "/test/endpoint"
2021 )
2122 var req *http.Request
23+ var trustedProxies *ip.NetSet
2224
2325 BeforeEach(func() {
26+ var err error
27+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
28+ Expect(err).ToNot(HaveOccurred())
29+
2430 req = httptest.NewRequest(
2531 http.MethodGet,
2632 fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
@@ -29,7 +35,7 @@ var _ = Describe("Util Suite", func() {
2935 })
3036
3137 Context("GetRequestHost", func() {
32− Context("IsProxied is false", func() {
38+ Context("trusted forwarded headers are disabled", func() {
3339 BeforeEach(func() {
3440 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
3541 })
@@ -44,10 +50,12 @@ var _ = Describe("Util Suite", func() {
4450 })
4551 })
4652
47− Context("IsProxied is true", func() {
53+ Context("trusted forwarded headers are enabled", func() {
4854 BeforeEach(func() {
55+ req.RemoteAddr = "127.0.0.1:4180"
4956 req = middleware.AddRequestScope(req, &middleware.RequestScope{
50− ReverseProxy: true,
57+ ReverseProxy: true,
58+ TrustedProxies: trustedProxies,
5159 })
5260 })
5361
@@ -63,7 +71,7 @@ var _ = Describe("Util Suite", func() {
6371 })
6472
6573 Context("GetRequestProto", func() {
66− Context("IsProxied is false", func() {
74+ Context("trusted forwarded headers are disabled", func() {
6775 BeforeEach(func() {
6876 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
6977 })
@@ -78,10 +86,12 @@ var _ = Describe("Util Suite", func() {
7886 })
7987 })
8088
81− Context("IsProxied is true", func() {
89+ Context("trusted forwarded headers are enabled", func() {
8290 BeforeEach(func() {
91+ req.RemoteAddr = "127.0.0.1:4180"
8392 req = middleware.AddRequestScope(req, &middleware.RequestScope{
84− ReverseProxy: true,
93+ ReverseProxy: true,
94+ TrustedProxies: trustedProxies,
8595 })
8696 })
8797
@@ -97,7 +107,7 @@ var _ = Describe("Util Suite", func() {
97107 })
98108
99109 Context("GetRequestURI", func() {
100− Context("IsProxied is false", func() {
110+ Context("trusted forwarded headers are disabled", func() {
101111 BeforeEach(func() {
102112 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
103113 })
@@ -112,10 +122,12 @@ var _ = Describe("Util Suite", func() {
112122 })
113123 })
114124
115− Context("IsProxied is true", func() {
125+ Context("trusted forwarded headers are enabled", func() {
116126 BeforeEach(func() {
127+ req.RemoteAddr = "127.0.0.1:4180"
117128 req = middleware.AddRequestScope(req, &middleware.RequestScope{
118− ReverseProxy: true,
129+ ReverseProxy: true,
130+ TrustedProxies: trustedProxies,
119131 })
120132 })
121133
@@ -131,7 +143,7 @@ var _ = Describe("Util Suite", func() {
131143 })
132144
133145 Context("GetRequestPath", func() {
134− Context("IsProxied is false", func() {
146+ Context("trusted forwarded headers are disabled", func() {
135147 BeforeEach(func() {
136148 req = middleware.AddRequestScope(req, &middleware.RequestScope{})
137149 })
@@ -146,10 +158,12 @@ var _ = Describe("Util Suite", func() {
146158 })
147159 })
148160
149− Context("IsProxied is true", func() {
161+ Context("trusted forwarded headers are enabled", func() {
150162 BeforeEach(func() {
163+ req.RemoteAddr = "127.0.0.1:4180"
151164 req = middleware.AddRequestScope(req, &middleware.RequestScope{
152− ReverseProxy: true,
165+ ReverseProxy: true,
166+ TrustedProxies: trustedProxies,
153167 })
154168 })
155169
@@ -163,4 +177,30 @@ var _ = Describe("Util Suite", func() {
163177 })
164178 })
165179 })
180+
181+ Context("CanTrustForwardedHeaders", func() {
182+ It("returns false when no scope is present", func() {
183+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
184+ })
185+
186+ It("returns true when the remote address is trusted", func() {
187+ req.RemoteAddr = "127.0.0.1:4180"
188+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
189+ ReverseProxy: true,
190+ TrustedProxies: trustedProxies,
191+ })
192+
193+ Expect(util.CanTrustForwardedHeaders(req)).To(BeTrue())
194+ })
195+
196+ It("returns false when the remote address is untrusted", func() {
197+ req.RemoteAddr = "192.0.2.10:4180"
198+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
199+ ReverseProxy: true,
200+ TrustedProxies: trustedProxies,
201+ })
202+
203+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
204+ })
205+ })
166206})
pkg/upstream/http_test.go+2−2
@@ -498,7 +498,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
498498
499499 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
500500
501− proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
501+ proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
502502 })
503503
504504 AfterEach(func() {
@@ -549,7 +549,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
549549 Expect(err).ToNot(HaveOccurred())
550550
551551 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
552− noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
552+ noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
553553 defer noPassHostServer.Close()
554554
555555 origin := "http://example.localhost"
pkg/validation/allowlist.go+12
@@ -16,6 +16,7 @@ func validateAllowlists(o *options.Options) []string {
1616
1717 msgs = append(msgs, validateAuthRoutes(o)...)
1818 msgs = append(msgs, validateAuthRegexes(o)...)
19+ msgs = append(msgs, validateTrustedProxyIPs(o)...)
1920 msgs = append(msgs, validateTrustedIPs(o)...)
2021
2122 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
@@ -28,6 +29,17 @@ func validateAllowlists(o *options.Options) []string {
2829 return msgs
2930}
3031
32+// validateTrustedProxyIPs validates IP/CIDRs for trusted reverse proxies.
33+func validateTrustedProxyIPs(o *options.Options) []string {
34+ msgs := []string{}
35+ for i, ipStr := range o.TrustedProxyIPs {
36+ if ip.ParseIPNet(ipStr) == nil {
37+ msgs = append(msgs, fmt.Sprintf("trusted_proxy_ips[%d] (%s) could not be recognized", i, ipStr))
38+ }
39+ }
40+ return msgs
41+}
42+
3143// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
3244func validateAuthRoutes(o *options.Options) []string {
3345 msgs := []string{}
pkg/validation/allowlist_test.go+30
@@ -23,6 +23,11 @@ var _ = Describe("Allowlist", func() {
2323 errStrings []string
2424 }
2525
26+ type validateTrustedProxyIPsTableInput struct {
27+ trustedProxyIPs []string
28+ errStrings []string
29+ }
30+
2631 DescribeTable("validateRoutes",
2732 func(r *validateRoutesTableInput) {
2833 opts := &options.Options{
@@ -121,4 +126,29 @@ var _ = Describe("Allowlist", func() {
121126 },
122127 }),
123128 )
129+
130+ DescribeTable("validateTrustedProxyIPs",
131+ func(t *validateTrustedProxyIPsTableInput) {
132+ opts := &options.Options{
133+ TrustedProxyIPs: t.trustedProxyIPs,
134+ }
135+ Expect(validateTrustedProxyIPs(opts)).To(ConsistOf(t.errStrings))
136+ },
137+ Entry("Valid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
138+ trustedProxyIPs: []string{
139+ "127.0.0.1",
140+ "10.32.0.1/32",
141+ "::1",
142+ "2a12:105:ee7:9234:0:0:0:0/64",
143+ },
144+ errStrings: []string{},
145+ }),
146+ Entry("Invalid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
147+ trustedProxyIPs: []string{"[::1]", "alkwlkbn/32"},
148+ errStrings: []string{
149+ "trusted_proxy_ips[0] ([::1]) could not be recognized",
150+ "trusted_proxy_ips[1] (alkwlkbn/32) could not be recognized",
151+ },
152+ }),
153+ )
124154})