diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7052049..320ba697 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,57 @@
## Breaking Changes
+## Changes since v7.15.2
+
+# V7.15.2
+
+## Release Highlights
+
+- 🔵 Golang version upgrade to v1.25.9
+ - Upgrade of all dependencies to their latest versions
+ - [CVE-2026-34986](https://nvd.nist.gov/vuln/detail/CVE-2026-34986)
+ - [CVE-2026-32281](https://nvd.nist.gov/vuln/detail/CVE-2026-32281)
+ - [CVE-2026-32289](https://nvd.nist.gov/vuln/detail/CVE-2026-32289)
+ - [CVE-2026-32288](https://nvd.nist.gov/vuln/detail/CVE-2026-32288)
+ - [CVE-2026-32280](https://nvd.nist.gov/vuln/detail/CVE-2026-32280)
+ - [CVE-2026-32282](https://nvd.nist.gov/vuln/detail/CVE-2026-32282)
+ - [CVE-2026-32283](https://nvd.nist.gov/vuln/detail/CVE-2026-32283)
+- 🕵️♀️ Vulnerabilities have been addressed
+
+## Important Notes
+
+We have had security audits performed on OAuth2 Proxy in the past couple of weeks and as a result we have fixed
+several CRITICAL vulnerabilities.
+
+The security vulnerabilities include multiple authentication bypasses and a potential session fixation attack.
+For more details and to identify if you are effects, we urge all users of OAuth2 Proxy to read the security
+disclosures.
+
+- (Critical) [GHSA-5hvv-m4w4-gf6v](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5hvv-m4w4-gf6v) fix: health check user-agent authentication bypass
+- (Critical) [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
+- (High) [GHSA-pxq7-h93f-9jrg](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-pxq7-h93f-9jrg) fix: fragment evaluation as part of the allowed routes
+- (Moderate) [GHSA-c5c4-8r6x-56w3](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-c5c4-8r6x-56w3) fix: email validation bypass via malformed multi-@ email claims
+
+Furthermore, for improving the security of OAuth2 Proxy we introduced a new flag `--trusted-proxy-ip` that allows users
+to explicitly specify trusted reverse proxy IPs for the `X-Forwarded-*` headers. This is an important step to prevent
+potential header spoofing attacks and to ensure that OAuth2 Proxy only trusts headers from known and trusted sources.
+We highly recommend users to review their deployment architecture and consider using this flag to enhance the security
+of their OAuth2 Proxy instances. Check the docs for more details: https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview#proxy-options
+
+Furthermore, we want to thank everyone who contributed to the audits and reported potential issues to make open source
+software like OAuth2 Proxy more secure for everyone.
+
+## Breaking Changes
+
## Changes since v7.15.1
-- [#3399](https://github.com/oauth2-proxy/oauth2-proxy/pull/3399) feat: add Redis client certificate and key paths for mutual TLS to the Redis session store (@karatkep)
+- [#3411](https://github.com/oauth2-proxy/oauth2-proxy/pull/3411) chore(deps): update gomod dependencies (@tuunit)
+- [#3333](https://github.com/oauth2-proxy/oauth2-proxy/pull/3333) fix: invalidate session on fatal OAuth2 refresh errors (@frhack)
+- [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)
+- [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)
+- [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)
+- [GHSA-pxq7-h93f-9jrg](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-pxq7-h93f-9jrg) fix: fragment evaluation as part of the allowed routes (@tuunit)
+- [GHSA-c5c4-8r6x-56w3](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-c5c4-8r6x-56w3) fix: email validation bypass via malformed multi-@ email claims (@tuunit)
# V7.15.1
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 71ec0fdc..2b98816c 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -6,7 +6,7 @@ by our [project governance](GOVERNANCE.md).
| Name | GitHub Handle | Domains of reponsibility | Email Alias | Affiliation |
| ---------------- | ------------------------------------------------------ | ------------------------ | -------------------------- | ----------- |
| Joel Speed | [@JoelSpeed](https://github.com/joelspeed) | Governance, Core | joel@oauth2-proxy.dev | Red Hat |
-| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | IONOS Cloud |
+| Jan Larwig | [@tuunit](https://github.com/tuunit) | Governance, Core | jan@oauth2-proxy.dev | STACKIT |
| JJ Łakis | [@jjlakis](https://github.com/jjlakis) | Provider | jj@oauth2-proxy.dev | - |
| Koen van Zuijlen | [@kvanzuijlen](https://github.com/kvanzuijlen) | CI | koen@oauth2-proxy.dev | - |
| Pierluigi Lenoci | [@pierluigilenoci](https://github.com/pierluigilenoci) | Helm | pierluigi@oauth2-proxy.dev | SAP |
diff --git a/contrib/local-environment/dex.yaml b/contrib/local-environment/dex.yaml
index f0a2ead4..e3ed0f8f 100644
--- a/contrib/local-environment/dex.yaml
+++ b/contrib/local-environment/dex.yaml
@@ -6,7 +6,7 @@ storage:
type: etcd
config:
endpoints:
- - http://etcd:2379
+ - http://etcd:2379
namespace: dex/
web:
http: 0.0.0.0:5556
@@ -16,17 +16,18 @@ expiry:
signingKeys: "4h"
idTokens: "1h"
staticClients:
-- id: oauth2-proxy
- redirectURIs:
- # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
- - 'http://oauth2-proxy.localtest.me:4180/oauth2/callback' # For basic proxy example.
- - 'http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback' # For nginx and traefik example.
- name: 'OAuth2 Proxy'
- secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
+ - id: oauth2-proxy
+ redirectURIs:
+ # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
+ - "http://oauth2-proxy.localtest.me:4180/oauth2/callback" # For basic proxy example.
+ - "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
+ - "http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" # For traefik example.
+ name: "OAuth2 Proxy"
+ secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
enablePasswordDB: true
staticPasswords:
-- email: "admin@example.com"
- # bcrypt hash of the string "password"
- hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
- username: "admin"
- userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
+ - email: "admin@example.com"
+ # bcrypt hash of the string "password"
+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
+ username: "admin"
+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
diff --git a/contrib/local-environment/docker-compose-alpha-config.yaml b/contrib/local-environment/docker-compose-alpha-config.yaml
index 515c42e0..2dde7345 100644
--- a/contrib/local-environment/docker-compose-alpha-config.yaml
+++ b/contrib/local-environment/docker-compose-alpha-config.yaml
@@ -14,7 +14,7 @@ version: "3.0"
services:
oauth2-proxy:
container_name: oauth2-proxy
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
hostname: oauth2-proxy
volumes:
diff --git a/contrib/local-environment/docker-compose-gitea.yaml b/contrib/local-environment/docker-compose-gitea.yaml
index 3e57ef2d..17d707fb 100644
--- a/contrib/local-environment/docker-compose-gitea.yaml
+++ b/contrib/local-environment/docker-compose-gitea.yaml
@@ -14,7 +14,7 @@ version: '3.0'
services:
oauth2-proxy:
container_name: oauth2-proxy
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
command: --config /oauth2-proxy.cfg
hostname: oauth2-proxy
volumes:
diff --git a/contrib/local-environment/docker-compose-keycloak.yaml b/contrib/local-environment/docker-compose-keycloak.yaml
index ba3db49a..70d2042b 100644
--- a/contrib/local-environment/docker-compose-keycloak.yaml
+++ b/contrib/local-environment/docker-compose-keycloak.yaml
@@ -10,11 +10,11 @@
#
# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings
-version: '3.0'
+version: "3.0"
services:
oauth2-proxy:
container_name: oauth2-proxy
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
command: --config /oauth2-proxy.cfg
hostname: oauth2-proxy
volumes:
@@ -43,9 +43,9 @@ services:
image: keycloak/keycloak:25.0
hostname: keycloak
command:
- - 'start-dev'
- - '--http-port=9080'
- - '--import-realm'
+ - "start-dev"
+ - "--http-port=9080"
+ - "--import-realm"
volumes:
- ./keycloak:/opt/keycloak/data/import
environment:
diff --git a/contrib/local-environment/docker-compose-nginx.yaml b/contrib/local-environment/docker-compose-nginx.yaml
index ed93d57c..2aa403ec 100644
--- a/contrib/local-environment/docker-compose-nginx.yaml
+++ b/contrib/local-environment/docker-compose-nginx.yaml
@@ -22,11 +22,12 @@
version: "3.0"
services:
oauth2-proxy:
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
ports: []
hostname: oauth2-proxy
container_name: oauth2-proxy
command: --config /oauth2-proxy.cfg
+ restart: unless-stopped
volumes:
- "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg"
networks:
@@ -44,7 +45,7 @@ services:
image: nginx:1.29
restart: unless-stopped
ports:
- - 80:80/tcp
+ - 8080:8080/tcp
hostname: nginx
volumes:
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
diff --git a/contrib/local-environment/docker-compose-traefik.yaml b/contrib/local-environment/docker-compose-traefik.yaml
index 94d9239b..302f1a42 100644
--- a/contrib/local-environment/docker-compose-traefik.yaml
+++ b/contrib/local-environment/docker-compose-traefik.yaml
@@ -23,7 +23,7 @@ version: '3.0'
services:
oauth2-proxy:
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
ports: []
hostname: oauth2-proxy
volumes:
diff --git a/contrib/local-environment/docker-compose.yaml b/contrib/local-environment/docker-compose.yaml
index 4832eb92..7630167d 100644
--- a/contrib/local-environment/docker-compose.yaml
+++ b/contrib/local-environment/docker-compose.yaml
@@ -13,7 +13,7 @@ version: "3.0"
services:
oauth2-proxy:
container_name: oauth2-proxy
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
command: --config /oauth2-proxy.cfg
hostname: oauth2-proxy
volumes:
diff --git a/contrib/local-environment/nginx.conf b/contrib/local-environment/nginx.conf
index 15005bf6..0e7bf7b4 100644
--- a/contrib/local-environment/nginx.conf
+++ b/contrib/local-environment/nginx.conf
@@ -1,91 +1,44 @@
-# Reverse proxy to oauth2-proxy
-server {
- listen 80;
- server_name oauth2-proxy.oauth2-proxy.localhost;
-
- location / {
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
-
- proxy_pass http://oauth2-proxy:4180/;
- }
-}
-
# Reverse proxy to httpbin
server {
- listen 80;
- server_name httpbin.oauth2-proxy.localhost;
+ listen 8080;
+ server_name oauth2-proxy.localtest.me;
- auth_request /internal-auth/oauth2/auth;
+ location /oauth2/ {
+ proxy_pass http://oauth2-proxy:4180;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Uri $request_uri;
+ proxy_set_header X-Auth-Request-Redirect $request_uri;
+ }
- # On 401, redirect to the sign_in page via a named location
- # This ensures a proper 302 redirect that browsers will follow
- error_page 401 = @oauth2_signin;
+ location = /oauth2/auth {
+ proxy_pass http://oauth2-proxy:4180;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Uri $request_uri;
+ # nginx auth_request includes headers but not body
+ proxy_set_header Content-Length "";
+ proxy_pass_request_body off;
+ }
location / {
- proxy_pass http://httpbin/;
- }
-
- # Named location for OAuth2 sign-in redirect
- # Returns a proper 302 that works with --skip-provider-button
- location @oauth2_signin {
- return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
- }
-
- # auth_request must be a URI so this allows an internal path to then proxy to
- # the real auth_request path.
- # The trailing /'s are required so that nginx strips the prefix before proxying.
- location /internal-auth/ {
- internal; # Ensure external users can't access this path
-
- # Make sure the OAuth2 Proxy knows where the original request came from.
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-Uri $request_uri;
-
- proxy_pass http://oauth2-proxy:4180/;
- }
-}
-
-# Statically serve the nginx welcome
-server {
- listen 80;
- server_name oauth2-proxy.localhost;
-
- location / {
- auth_request /internal-auth/oauth2/auth;
-
- # On 401, redirect to the sign_in page via a named location
- # This ensures a proper 302 redirect that browsers will follow
+ auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
- root /usr/share/nginx/html;
- index index.html index.htm;
+ # pass information via X-User and X-Email headers to backend,
+ # requires running with --set-xauthrequest flag
+ auth_request_set $user $upstream_http_x_auth_request_user;
+ auth_request_set $email $upstream_http_x_auth_request_email;
+ proxy_set_header X-User $user;
+ proxy_set_header X-Email $email;
+
+ proxy_pass http://httpbin/;
+ # or "root /path/to/site;" or "fastcgi_pass ..." etc
}
- # Named location for OAuth2 sign-in redirect
- # Returns a proper 302 that works with --skip-provider-button
+ # Named location for handling OAuth2 sign-in redirects
+ # This ensures the browser receives a proper 302 redirect that it will follow
location @oauth2_signin {
- return 302 http://oauth2-proxy.oauth2-proxy.localhost/oauth2/sign_in?rd=$scheme://$host$request_uri;
- }
-
- # redirect server error pages to the static page /50x.html
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
-
- # auth_request must be a URI so this allows an internal path to then proxy to
- # the real auth_request path.
- # The trailing /'s are required so that nginx strips the prefix before proxying.
- location /internal-auth/ {
- internal; # Ensure external users can't access this path
-
- # Make sure the OAuth2 Proxy knows where the original request came from.
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-Uri $request_uri;
-
- proxy_pass http://oauth2-proxy:4180/;
+ return 302 /oauth2/sign_in?rd=$scheme://$http_host$request_uri;
}
}
diff --git a/contrib/local-environment/oauth2-proxy-nginx.cfg b/contrib/local-environment/oauth2-proxy-nginx.cfg
index 01b64a55..2565c226 100644
--- a/contrib/local-environment/oauth2-proxy-nginx.cfg
+++ b/contrib/local-environment/oauth2-proxy-nginx.cfg
@@ -1,14 +1,19 @@
http_address="0.0.0.0:4180"
cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
-provider="oidc"
email_domains="example.com"
-oidc_issuer_url="http://dex.localtest.me:5556/dex"
+cookie_secure="false"
+upstreams="static://200"
+cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains.
+whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target.
+
+# dex provider
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
client_id="oauth2-proxy"
-cookie_secure="false"
+redirect_url="http://oauth2-proxy.localtest.me:8080/oauth2/callback"
+
+oidc_issuer_url="http://dex.localtest.me:5556/dex"
+provider="oidc"
+provider_display_name="Dex"
-redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
-cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
-whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
# Enables the use of `X-Forwarded-*` headers to determine request correctly
reverse_proxy="true"
diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md
index a73e3acd..965953fa 100644
--- a/docs/docs/configuration/overview.md
+++ b/docs/docs/configuration/overview.md
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
### Proxy Options
+:::warning
+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.
+:::
+
| Flag / Config Field | Type | Description | Default |
| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| flag: `--allow-query-semicolons`
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,10 +215,11 @@ Provider specific options can be found on their respective subpages.
| flag: `--redirect-url`
toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
| flag: `--relative-redirect-url`
toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
| flag: `--reverse-proxy`
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 |
+| flag: `--trusted-proxy-ip`
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"` |
| flag: `--signature-key`
toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
| flag: `--skip-auth-preflight`
toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
-| flag: `--skip-auth-regex`
toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
-| flag: `--skip-auth-route`
toml: `skip_auth_routes` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
+| flag: `--skip-auth-regex`
toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times). Path matching is performed against the normalized path only; fragment identifiers (`#`) and their URL-encoded form (`%23`) are stripped before evaluation. | |
+| flag: `--skip-auth-route`
toml: `skip_auth_routes` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex. Path matching is performed against the normalized path only; fragment identifiers (`#`) and their URL-encoded form (`%23`) are stripped before evaluation. | |
| flag: `--skip-jwt-bearer-tokens`
toml: `skip_jwt_bearer_tokens` | bool | will skip requests that have verified JWT bearer tokens (the token must have [`aud`](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) that matches this client id or one of the extras from `extra-jwt-issuers`) | false |
| flag: `--skip-provider-button`
toml: `skip_provider_button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
| flag: `--ssl-insecure-skip-verify`
toml: `ssl_insecure_skip_verify` | bool | skip validation of certificates presented when using HTTPS providers | false |
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
index d329bd55..7898f70c 100644
--- a/docs/docs/installation.md
+++ b/docs/docs/installation.md
@@ -5,7 +5,7 @@ title: Installation
1. Choose how to deploy:
- a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.2`)
b. Using Go to install the latest release
```bash
diff --git a/docs/package.json b/docs/package.json
index a288a213..0bb4b494 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -42,5 +42,8 @@
},
"engines": {
"node": ">=18.0"
+ },
+ "overrides" : {
+ "webpackbar" : "^7.0.0"
}
}
diff --git a/docs/versioned_docs/version-7.15.x/configuration/overview.md b/docs/versioned_docs/version-7.15.x/configuration/overview.md
index a73e3acd..b145065a 100644
--- a/docs/versioned_docs/version-7.15.x/configuration/overview.md
+++ b/docs/versioned_docs/version-7.15.x/configuration/overview.md
@@ -193,6 +193,10 @@ Provider specific options can be found on their respective subpages.
### Proxy Options
+:::warning
+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.
+:::
+
| Flag / Config Field | Type | Description | Default |
| ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| flag: `--allow-query-semicolons`
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.
| flag: `--redirect-url`
toml: `redirect_url` | string | the OAuth Redirect URL, e.g. `"https://internalapp.yourcompany.com/oauth2/callback"` | |
| flag: `--relative-redirect-url`
toml: `relative_redirect_url` | bool | allow relative OAuth Redirect URL.` | false |
| flag: `--reverse-proxy`
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 |
+| flag: `--trusted-proxy-ip`
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"` |
| flag: `--signature-key`
toml: `signature_key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
| flag: `--skip-auth-preflight`
toml: `skip_auth_preflight` | bool | will skip authentication for OPTIONS requests | false |
| flag: `--skip-auth-regex`
toml: `skip_auth_regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
diff --git a/docs/versioned_docs/version-7.15.x/installation.md b/docs/versioned_docs/version-7.15.x/installation.md
index d329bd55..7898f70c 100644
--- a/docs/versioned_docs/version-7.15.x/installation.md
+++ b/docs/versioned_docs/version-7.15.x/installation.md
@@ -5,7 +5,7 @@ title: Installation
1. Choose how to deploy:
- a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.1`)
+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.2`)
b. Using Go to install the latest release
```bash
diff --git a/go.mod b/go.mod
index d45e4b6d..ade9c4e8 100644
--- a/go.mod
+++ b/go.mod
@@ -9,12 +9,12 @@ require (
github.com/alicebob/miniredis/v2 v2.37.0
github.com/bitly/go-simplejson v0.5.1
github.com/bsm/redislock v0.9.4
- github.com/coreos/go-oidc/v3 v3.17.0
+ github.com/coreos/go-oidc/v3 v3.18.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/fsnotify/fsnotify v1.9.0
- github.com/go-jose/go-jose/v3 v3.0.4
+ github.com/go-jose/go-jose/v3 v3.0.5
github.com/go-jose/go-jose/v4 v4.1.4
- github.com/go-viper/mapstructure/v2 v2.4.0
+ github.com/go-viper/mapstructure/v2 v2.5.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
@@ -32,17 +32,17 @@ require (
github.com/stretchr/testify v1.11.1
github.com/vmihailenco/msgpack/v5 v5.4.1
go.yaml.in/yaml/v3 v3.0.4
- golang.org/x/crypto v0.49.0
- golang.org/x/net v0.52.0
+ golang.org/x/crypto v0.50.0
+ golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
- google.golang.org/api v0.272.0
+ google.golang.org/api v0.275.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
k8s.io/apimachinery v0.35.3
)
require (
- cloud.google.com/go/auth v0.18.2 // indirect
+ cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -53,13 +53,13 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
- github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
+ github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
- github.com/googleapis/gax-go/v2 v2.19.0 // indirect
+ github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
@@ -70,18 +70,18 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
- go.opentelemetry.io/otel v1.42.0 // indirect
- go.opentelemetry.io/otel/metric v1.42.0 // indirect
- go.opentelemetry.io/otel/trace v1.42.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
+ go.opentelemetry.io/otel v1.43.0 // indirect
+ go.opentelemetry.io/otel/metric v1.43.0 // indirect
+ go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
- golang.org/x/tools v0.43.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
- google.golang.org/grpc v1.79.3 // indirect
+ golang.org/x/mod v0.35.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
+ golang.org/x/tools v0.44.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
+ google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index ea2aab21..622f2dac 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
+cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
+cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -33,6 +35,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
+github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
+github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -54,6 +58,8 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
+github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -65,6 +71,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -78,6 +86,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
+github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -87,6 +97,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
+github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
+github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
@@ -119,6 +131,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
+github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -175,18 +189,29 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
+go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
+go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
+go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
+go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
+go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
+go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
+go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
+go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -200,10 +225,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
+golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -211,6 +240,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -229,6 +260,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -242,6 +275,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -250,19 +285,30 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
+golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
+golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
+google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI=
+google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
+google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
+google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/main_test.go b/main_test.go
index c7c7057d..58b8ae7e 100644
--- a/main_test.go
+++ b/main_test.go
@@ -278,7 +278,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
Entry("with bad legacy configuration", loadConfigurationTableInput{
configContent: testCoreConfig + "unknown_field=\"something\"",
expectedOptions: func() *options.Options { return nil },
- expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"),
+ expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'options.LegacyOptions' has invalid keys: unknown_field"),
}),
Entry("with bad alpha configuration", loadConfigurationTableInput{
configContent: testCoreConfig,
@@ -290,7 +290,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
configContent: testCoreConfig + "unknown_field=\"something\"",
alphaConfigContent: testAlphaConfig,
expectedOptions: func() *options.Options { return nil },
- expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"),
+ expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'options.LegacyOptions' has invalid keys: unknown_field"),
}),
)
diff --git a/oauthproxy.go b/oauthproxy.go
index 3efe66fd..e2357c8d 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -59,6 +59,8 @@ const (
)
var (
+ defaultTrustedProxyIPs = []string{"0.0.0.0/0", "::/0"}
+
// ErrNeedsLogin means the user should be redirected to the login page
ErrNeedsLogin = errors.New("redirect to login page")
@@ -183,13 +185,14 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
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)
- trustedIPs := ip.NewNetSet()
- for _, ipStr := range opts.TrustedIPs {
- if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
- trustedIPs.AddIPNet(*ipNet)
- } else {
- return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
- }
+ trustedIPs, err := ip.ParseNetSet(opts.TrustedIPs)
+ if err != nil {
+ return nil, err
+ }
+
+ trustedProxies, err := buildTrustedProxyNetSet(opts)
+ if err != nil {
+ return nil, err
}
allowedRoutes, err := buildRoutesAllowlist(opts)
@@ -202,7 +205,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
return nil, err
}
- preAuthChain, err := buildPreAuthChain(opts, sessionStore)
+ preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
if err != nil {
return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
}
@@ -355,8 +358,8 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
// buildPreAuthChain constructs a chain that should process every request before
// the OAuth2 Proxy authentication logic kicks in.
// For example forcing HTTPS or health checks.
-func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
- chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
+func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) {
+ chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
if opts.ForceHTTPS {
_, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress)
@@ -395,6 +398,16 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt
return chain, nil
}
+func buildTrustedProxyNetSet(opts *options.Options) (*ip.NetSet, error) {
+ trustedProxyIPs := opts.TrustedProxyIPs
+ if opts.ReverseProxy && len(trustedProxyIPs) == 0 {
+ 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.")
+ trustedProxyIPs = defaultTrustedProxyIPs
+ }
+
+ return ip.ParseNetSet(trustedProxyIPs)
+}
+
func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
chain := alice.New()
@@ -634,6 +647,10 @@ func (p *OAuthProxy) isTrustedIP(req *http.Request) bool {
// SignInPage writes the sign in template to the response
func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
prepareNoCache(rw)
+
+ if err := p.ClearSessionCookie(rw, req); err != nil {
+ logger.Printf("Error clearing session cookie: %v", err)
+ }
rw.WriteHeader(code)
redirectURL, err := p.appDirector.GetRedirect(req)
@@ -647,10 +664,6 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
redirectURL = "/"
}
- if err := p.ClearSessionCookie(rw, req); err != nil {
- logger.Printf("Error clearing session cookie: %v", err)
- }
-
p.pageWriter.WriteSignInPage(rw, req, redirectURL, code)
}
diff --git a/oauthproxy_test.go b/oauthproxy_test.go
index e06f50e9..e1235a4e 100644
--- a/oauthproxy_test.go
+++ b/oauthproxy_test.go
@@ -713,6 +713,50 @@ func TestManualSignInCorrectCredentials(t *testing.T) {
assert.Equal(t, http.StatusFound, statusCode)
}
+func TestSignInPageClearsExistingSessionCookie(t *testing.T) {
+ opts := baseTestOptions()
+ err := validation.Validate(opts)
+ require.NoError(t, err)
+
+ proxy, err := NewOAuthProxy(opts, func(string) bool {
+ return true
+ })
+ require.NoError(t, err)
+
+ // Create a real session cookie using the actual session store.
+ saveRW := httptest.NewRecorder()
+ saveReq := httptest.NewRequest(http.MethodGet, "/", nil)
+ err = proxy.sessionStore.Save(saveRW, saveReq, &sessions.SessionState{
+ Email: "john.doe@example.com",
+ })
+ require.NoError(t, err)
+
+ cookies := saveRW.Result().Cookies()
+ require.NotEmpty(t, cookies)
+
+ // Send that cookie to the sign-in page.
+ req := httptest.NewRequest(http.MethodGet, "/oauth2/sign_in", nil)
+ for _, c := range cookies {
+ req.AddCookie(c)
+ }
+
+ rw := httptest.NewRecorder()
+ proxy.ServeHTTP(rw, req)
+
+ assert.Equal(t, http.StatusOK, rw.Code)
+
+ cleared := false
+ for _, c := range rw.Result().Cookies() {
+ if c.Name == proxy.CookieOptions.Name {
+ cleared = true
+ assert.Equal(t, "", c.Value)
+ assert.Less(t, c.MaxAge, 0)
+ }
+ }
+
+ assert.True(t, cleared, "expected sign-in page to clear existing session cookie")
+}
+
func TestSignInPageIncludesTargetRedirect(t *testing.T) {
sipTest, err := NewSignInPageTest(false)
if err != nil {
@@ -2635,9 +2679,11 @@ func TestAllowedRequest(t *testing.T) {
}
opts.SkipAuthRegex = []string{
"^/skip/auth/regex$",
+ "^/public/.*/endpoint$",
}
opts.SkipAuthRoutes = []string{
"GET=^/skip/auth/routes/get",
+ "^/foo/.*/bar$",
}
err := validation.Validate(opts)
assert.NoError(t, err)
@@ -2670,6 +2716,18 @@ func TestAllowedRequest(t *testing.T) {
url: "/wrong/denied",
allowed: false,
},
+ {
+ name: "Regex allowed with fragment-free path",
+ method: "GET",
+ url: "/public/legit/endpoint",
+ allowed: true,
+ },
+ {
+ name: "Regex denied when path contains encoded fragment suffix",
+ method: "GET",
+ url: "/public/secret%23/endpoint",
+ allowed: false,
+ },
{
name: "Route allowed",
method: "GET",
@@ -2694,6 +2752,18 @@ func TestAllowedRequest(t *testing.T) {
url: "/skip/auth/routes/wrong/path",
allowed: false,
},
+ {
+ name: "Route allowed with fragment-free path",
+ method: "GET",
+ url: "/foo/public/bar",
+ allowed: true,
+ },
+ {
+ name: "Route denied when path contains encoded fragment suffix",
+ method: "GET",
+ url: "/foo/secret%23/bar",
+ allowed: false,
+ },
}
for _, tc := range testCases {
@@ -2734,9 +2804,11 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
}
opts.SkipAuthRegex = []string{
"^/skip/auth/regex$",
+ "^/public/.*/endpoint$",
}
opts.SkipAuthRoutes = []string{
"GET=^/skip/auth/routes/get",
+ "^/foo/.*/bar$",
}
err := validation.Validate(opts)
assert.NoError(t, err)
@@ -2769,6 +2841,18 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
url: "/wrong/denied",
allowed: false,
},
+ {
+ name: "Regex allowed with fragment-free path",
+ method: "GET",
+ url: "/public/legit/endpoint",
+ allowed: true,
+ },
+ {
+ name: "Regex denied when X-Forwarded-Uri contains an encoded fragment suffix",
+ method: "GET",
+ url: "/public/secret%23/endpoint",
+ allowed: false,
+ },
{
name: "Route allowed",
method: "GET",
@@ -2793,12 +2877,25 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
url: "/skip/auth/routes/wrong/path",
allowed: false,
},
+ {
+ name: "Route allowed with fragment-free path",
+ method: "GET",
+ url: "/foo/public/bar",
+ allowed: true,
+ },
+ {
+ name: "Route denied when X-Forwarded-Uri contains an encoded fragment suffix",
+ method: "GET",
+ url: "/foo/secret%23/bar",
+ allowed: false,
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil)
req.Header.Set("X-Forwarded-Uri", tc.url)
+ req.RemoteAddr = "127.0.0.1:4180"
assert.NoError(t, err)
rw := httptest.NewRecorder()
@@ -2813,6 +2910,43 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) {
}
}
+func TestAllowedRequestWithForwardedUriHeaderRequiresTrustedProxy(t *testing.T) {
+ upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(200)
+ }))
+ t.Cleanup(upstreamServer.Close)
+
+ opts := baseTestOptions()
+ opts.ReverseProxy = true
+ opts.TrustedProxyIPs = []string{"127.0.0.1/32"}
+ opts.UpstreamServers = options.UpstreamConfig{
+ Upstreams: []options.Upstream{
+ {
+ ID: upstreamServer.URL,
+ Path: "/",
+ URI: upstreamServer.URL,
+ },
+ },
+ }
+ opts.SkipAuthRegex = []string{"^/skip/auth/regex$"}
+
+ err := validation.Validate(opts)
+ assert.NoError(t, err)
+
+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
+ assert.NoError(t, err)
+
+ req, err := http.NewRequest(http.MethodGet, opts.ProxyPrefix+authOnlyPath, nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = "192.0.2.10:4180"
+ req.Header.Set("X-Forwarded-Uri", "/skip/auth/regex")
+
+ rw := httptest.NewRecorder()
+ proxy.ServeHTTP(rw, req)
+
+ assert.Equal(t, 401, rw.Code)
+}
+
func TestAllowedRequestNegateWithoutMethod(t *testing.T) {
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
@@ -3404,6 +3538,24 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
expectedStatusCode: http.StatusAccepted,
},
+ {
+ name: "UserWithMultipleAtSignsExactDomain",
+ email: "attacker@evil.com@example.com",
+ querystring: "?allowed_email_domains=example.com",
+ expectedStatusCode: http.StatusForbidden,
+ },
+ {
+ name: "UserWithMultipleAtSignsWildcardDomain",
+ email: "attacker@evil.com@foo.example.com",
+ querystring: "?allowed_email_domains=*.example.com",
+ expectedStatusCode: http.StatusForbidden,
+ },
+ {
+ name: "UserWithMultipleAtSignsDotPrefixedDomain",
+ email: "attacker@evil.com@foo.example.com",
+ querystring: "?allowed_email_domains=.example.com",
+ expectedStatusCode: http.StatusForbidden,
+ },
}
for _, tc := range testCases {
diff --git a/pkg/apis/middleware/scope.go b/pkg/apis/middleware/scope.go
index 2d84f00e..b778bd54 100644
--- a/pkg/apis/middleware/scope.go
+++ b/pkg/apis/middleware/scope.go
@@ -2,9 +2,12 @@ package middleware
import (
"context"
+ "net"
"net/http"
+ "strings"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
)
type scopeKey string
@@ -18,9 +21,13 @@ const RequestScopeKey scopeKey = "request-scope"
// within the chain.
type RequestScope struct {
// ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy
- // mode and if request `X-Forwarded-*` headers should be trusted
+ // mode and if request `X-Forwarded-*` headers may be trusted
ReverseProxy bool
+ // TrustedProxies tracks which direct callers are allowed to supply
+ // forwarded headers when ReverseProxy mode is enabled.
+ TrustedProxies *ip.NetSet
+
// RequestID is set to the request's `X-Request-Id` header if set.
// Otherwise a random UUID is set.
RequestID string
@@ -58,3 +65,43 @@ func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request {
ctx := context.WithValue(req.Context(), RequestScopeKey, scope)
return req.WithContext(ctx)
}
+
+// CanTrustForwardedHeaders returns whether forwarded headers should be
+// processed for this request.
+func (s *RequestScope) CanTrustForwardedHeaders(req *http.Request) bool {
+ if s == nil || req == nil || !s.ReverseProxy || s.TrustedProxies == nil {
+ return false
+ }
+
+ if isUnixSocketRemoteAddr(req.RemoteAddr) {
+ return true
+ }
+
+ remoteIP := parseRemoteAddrIP(req.RemoteAddr)
+ if remoteIP == nil {
+ return false
+ }
+
+ return s.TrustedProxies.Has(remoteIP)
+}
+
+func parseRemoteAddrIP(remoteAddr string) net.IP {
+ if remoteAddr == "" {
+ return nil
+ }
+
+ if ip := net.ParseIP(remoteAddr); ip != nil {
+ return ip
+ }
+
+ host, _, err := net.SplitHostPort(remoteAddr)
+ if err != nil {
+ return nil
+ }
+
+ return net.ParseIP(host)
+}
+
+func isUnixSocketRemoteAddr(remoteAddr string) bool {
+ return remoteAddr == "@" || strings.HasPrefix(remoteAddr, "/")
+}
diff --git a/pkg/apis/middleware/scope_test.go b/pkg/apis/middleware/scope_test.go
index f1845518..ec485b47 100644
--- a/pkg/apis/middleware/scope_test.go
+++ b/pkg/apis/middleware/scope_test.go
@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -53,4 +54,37 @@ var _ = Describe("Scope Suite", func() {
})
})
})
+
+ Context("CanTrustForwardedHeaders", func() {
+ var request *http.Request
+ var scope *middleware.RequestScope
+
+ BeforeEach(func() {
+ var err error
+ request, err = http.NewRequest("", "http://127.0.0.1/", nil)
+ Expect(err).ToNot(HaveOccurred())
+
+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+ scope = &middleware.RequestScope{
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
+ }
+ })
+
+ It("returns true for a trusted remote address", func() {
+ request.RemoteAddr = "127.0.0.1:4180"
+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
+ })
+
+ It("returns false for an untrusted remote address", func() {
+ request.RemoteAddr = "192.0.2.10:4180"
+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeFalse())
+ })
+
+ It("returns true for unix socket callers", func() {
+ request.RemoteAddr = "@"
+ Expect(scope.CanTrustForwardedHeaders(request)).To(BeTrue())
+ })
+ })
})
diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go
index 42083f76..40f9a725 100644
--- a/pkg/apis/options/load_test.go
+++ b/pkg/apis/options/load_test.go
@@ -329,7 +329,7 @@ var _ = Describe("Load", func() {
Entry("with an unknown option in the config file", &testOptionsTableInput{
configFile: []byte(`unknown_option="foo"`),
flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
- expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_option"),
+ expectedErr: fmt.Errorf("error unmarshalling config: decoding failed due to the following error(s):\n\n'options.TestOptions' has invalid keys: unknown_option"),
// Viper will unmarshal before returning the error, so this is the default output
expectedOutput: &TestOptions{
StringOption: "default",
diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go
index 2ca3a766..2756014b 100644
--- a/pkg/apis/options/options.go
+++ b/pkg/apis/options/options.go
@@ -24,6 +24,7 @@ type Options struct {
ReadyPath string `flag:"ready-path" cfg:"ready_path"`
ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
+ TrustedProxyIPs []string `flag:"trusted-proxy-ip" cfg:"trusted_proxy_ips"`
TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
@@ -119,6 +120,7 @@ func NewFlagSet() *pflag.FlagSet {
flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
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)")
+ 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.")
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.")
flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
diff --git a/pkg/app/redirect/director_test.go b/pkg/app/redirect/director_test.go
index 69c01d95..58d0e8c3 100644
--- a/pkg/app/redirect/director_test.go
+++ b/pkg/app/redirect/director_test.go
@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -33,9 +34,16 @@ var _ = Describe("Director Suite", func() {
req.Header.Add(header, value)
}
}
- req = middleware.AddRequestScope(req, &middleware.RequestScope{
+ scope := &middleware.RequestScope{
ReverseProxy: in.reverseProxy,
- })
+ }
+ if in.reverseProxy {
+ req.RemoteAddr = "127.0.0.1:4180"
+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+ scope.TrustedProxies = trustedProxies
+ }
+ req = middleware.AddRequestScope(req, scope)
redirect, err := appDirector.GetRedirect(req)
Expect(err).ToNot(HaveOccurred())
@@ -174,4 +182,27 @@ var _ = Describe("Director Suite", func() {
expectedRedirect: "https://a-service.example.com/foo/bar",
}),
)
+
+ It("ignores forwarded headers from an untrusted remote address", func() {
+ appDirector := NewAppDirector(AppDirectorOpts{
+ ProxyPrefix: testProxyPrefix,
+ Validator: testValidator(true),
+ })
+
+ req, _ := http.NewRequest("GET", "https://oauth.example.com/foo?bar", nil)
+ req.RemoteAddr = "192.0.2.10:4180"
+ req.Header.Add("X-Forwarded-Proto", "https")
+ req.Header.Add("X-Forwarded-Host", "a-service.example.com")
+ req.Header.Add("X-Forwarded-Uri", fooBar)
+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
+ })
+
+ redirect, err := appDirector.GetRedirect(req)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(redirect).To(Equal("/foo?bar"))
+ })
})
diff --git a/pkg/cookies/cookies_test.go b/pkg/cookies/cookies_test.go
index b67f8a69..ba8b5480 100644
--- a/pkg/cookies/cookies_test.go
+++ b/pkg/cookies/cookies_test.go
@@ -6,6 +6,7 @@ import (
"time"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -30,8 +31,13 @@ var _ = Describe("Cookie Tests", func() {
if in.xForwardedHost != "" {
req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
+ req.RemoteAddr = "127.0.0.1:4180"
+ trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+
req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
- ReverseProxy: true,
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
})
}
diff --git a/pkg/ip/parse_ip_net.go b/pkg/ip/parse_ip_net.go
index 9cb37de2..4fc63664 100644
--- a/pkg/ip/parse_ip_net.go
+++ b/pkg/ip/parse_ip_net.go
@@ -1,6 +1,7 @@
package ip
import (
+ "fmt"
"net"
"strings"
)
@@ -37,3 +38,18 @@ func ParseIPNet(s string) *net.IPNet {
return ipNet
}
}
+
+func ParseNetSet(ipStrs []string) (*NetSet, error) {
+ netSet := NewNetSet()
+
+ for _, ipStr := range ipStrs {
+ ipNet := ParseIPNet(ipStr)
+ if ipNet == nil {
+ return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
+ }
+
+ netSet.AddIPNet(*ipNet)
+ }
+
+ return netSet, nil
+}
diff --git a/pkg/ip/parse_ip_net_test.go b/pkg/ip/parse_ip_net_test.go
new file mode 100644
index 00000000..41c1b4b9
--- /dev/null
+++ b/pkg/ip/parse_ip_net_test.go
@@ -0,0 +1,118 @@
+package ip
+
+import (
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseIPNet(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expectedIP net.IP
+ expectedMask net.IPMask
+ }{
+ {
+ name: "ipv4 address",
+ input: "127.0.0.1",
+ expectedIP: net.ParseIP("127.0.0.1"),
+ expectedMask: net.CIDRMask(32, 32),
+ },
+ {
+ name: "ipv6 address",
+ input: "::1",
+ expectedIP: net.ParseIP("::1"),
+ expectedMask: net.CIDRMask(128, 128),
+ },
+ {
+ name: "ipv4 cidr",
+ input: "10.0.0.0/24",
+ expectedIP: net.ParseIP("10.0.0.0"),
+ expectedMask: net.CIDRMask(24, 32),
+ },
+ {
+ name: "ipv6 cidr",
+ input: "2001:db8::/64",
+ expectedIP: net.ParseIP("2001:db8::"),
+ expectedMask: net.CIDRMask(64, 128),
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ ipNet := ParseIPNet(test.input)
+
+ assert.NotNil(t, ipNet)
+ if ipNet == nil {
+ return
+ }
+
+ assert.True(t, test.expectedIP.Equal(ipNet.IP))
+ assert.Equal(t, test.expectedMask, ipNet.Mask)
+ })
+ }
+}
+
+func TestParseIPNetRejectsInvalidNetworks(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ }{
+ {
+ name: "invalid ip",
+ input: "not-an-ip",
+ },
+ {
+ name: "ipv4 cidr with host bits set",
+ input: "10.0.0.1/24",
+ },
+ {
+ name: "ipv6 cidr with host bits set",
+ input: "2001:db8::1/64",
+ },
+ {
+ name: "invalid cidr mask",
+ input: "10.0.0.0/33",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Nil(t, ParseIPNet(test.input))
+ })
+ }
+}
+
+func TestParseNetSet(t *testing.T) {
+ netSet, err := ParseNetSet([]string{
+ "127.0.0.1",
+ "10.0.0.0/24",
+ "::1",
+ "2001:db8::/64",
+ })
+
+ assert.NoError(t, err)
+ assert.NotNil(t, netSet)
+ if netSet == nil {
+ return
+ }
+
+ assert.True(t, netSet.Has(net.ParseIP("127.0.0.1")))
+ assert.True(t, netSet.Has(net.ParseIP("10.0.0.55")))
+ assert.True(t, netSet.Has(net.ParseIP("::1")))
+ assert.True(t, netSet.Has(net.ParseIP("2001:db8::abcd")))
+
+ assert.False(t, netSet.Has(net.ParseIP("127.0.0.2")))
+ assert.False(t, netSet.Has(net.ParseIP("10.0.1.1")))
+ assert.False(t, netSet.Has(net.ParseIP("::2")))
+ assert.False(t, netSet.Has(net.ParseIP("2001:db9::1")))
+}
+
+func TestParseNetSetReturnsErrorForInvalidNetwork(t *testing.T) {
+ netSet, err := ParseNetSet([]string{"127.0.0.1", "10.0.0.1/24"})
+
+ assert.Nil(t, netSet)
+ assert.EqualError(t, err, "could not parse IP network (10.0.0.1/24)")
+}
diff --git a/pkg/middleware/healthcheck.go b/pkg/middleware/healthcheck.go
index 2dcfc1d4..de3b63d2 100644
--- a/pkg/middleware/healthcheck.go
+++ b/pkg/middleware/healthcheck.go
@@ -43,10 +43,13 @@ func healthCheck(paths, userAgents []string, next http.Handler) http.Handler {
func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
if _, ok := paths[req.URL.EscapedPath()]; ok {
- return true
- }
- if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
- return true
+ if len(userAgents) == 0 {
+ return true
+ }
+
+ if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
+ return true
+ }
}
return false
}
diff --git a/pkg/middleware/healthcheck_test.go b/pkg/middleware/healthcheck_test.go
index 78e1e6d4..68a8d3ec 100644
--- a/pkg/middleware/healthcheck_test.go
+++ b/pkg/middleware/healthcheck_test.go
@@ -45,6 +45,16 @@ var _ = Describe("HealthCheck suite", func() {
healthCheckPaths: []string{"/ping"},
healthCheckUserAgents: []string{"hc/1.0"},
requestString: "http://example.com/ping",
+ headers: map[string]string{
+ "User-Agent": "hc/1.0",
+ },
+ expectedStatus: 200,
+ expectedBody: "OK",
+ }),
+ Entry("when requesting the healthcheck path with no health check user agents configured", &requestTableInput{
+ healthCheckPaths: []string{"/ping"},
+ healthCheckUserAgents: []string{},
+ requestString: "http://example.com/ping",
headers: map[string]string{},
expectedStatus: 200,
expectedBody: "OK",
@@ -85,15 +95,25 @@ var _ = Describe("HealthCheck suite", func() {
expectedStatus: 404,
expectedBody: "404 page not found\n",
}),
- Entry("with a request from the health check user agent", &requestTableInput{
+ Entry("with a request from the health check user agent on a non-healthcheck path", &requestTableInput{
healthCheckPaths: []string{"/ping"},
healthCheckUserAgents: []string{"hc/1.0"},
requestString: "http://example.com/abc",
headers: map[string]string{
"User-Agent": "hc/1.0",
},
- expectedStatus: 200,
- expectedBody: "OK",
+ expectedStatus: 404,
+ expectedBody: "404 page not found\n",
+ }),
+ Entry("when an auth_request endpoint receives the configured health check user agent", &requestTableInput{
+ healthCheckPaths: []string{"/ping"},
+ healthCheckUserAgents: []string{"GoogleHC/1.0"},
+ requestString: "http://example.com/oauth2/auth",
+ headers: map[string]string{
+ "User-Agent": "GoogleHC/1.0",
+ },
+ expectedStatus: 404,
+ expectedBody: "404 page not found\n",
}),
Entry("when a blank string is configured as a health check agent and a request has no user agent", &requestTableInput{
healthCheckPaths: []string{"/ping"},
@@ -107,9 +127,11 @@ var _ = Describe("HealthCheck suite", func() {
healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
healthCheckUserAgents: []string{"hc/1.0"},
requestString: "http://example.com/readiness_check",
- headers: map[string]string{},
- expectedStatus: 200,
- expectedBody: "OK",
+ headers: map[string]string{
+ "User-Agent": "hc/1.0",
+ },
+ expectedStatus: 200,
+ expectedBody: "OK",
}),
Entry("with multiple paths, request none of the healthcheck paths", &requestTableInput{
healthCheckPaths: []string{"/ping", "/liveness_check", "/readiness_check"},
@@ -121,15 +143,15 @@ var _ = Describe("HealthCheck suite", func() {
expectedStatus: 404,
expectedBody: "404 page not found\n",
}),
- Entry("with multiple user agents, request from a health check user agent", &requestTableInput{
+ Entry("with multiple user agents, request from a health check user agent on a non-healthcheck path", &requestTableInput{
healthCheckPaths: []string{"/ping"},
healthCheckUserAgents: []string{"hc/1.0", "GoogleHC/1.0"},
requestString: "http://example.com/abc",
headers: map[string]string{
"User-Agent": "GoogleHC/1.0",
},
- expectedStatus: 200,
- expectedBody: "OK",
+ expectedStatus: 404,
+ expectedBody: "404 page not found\n",
}),
Entry("with multiple user agents, request from none of the health check user agents", &requestTableInput{
healthCheckPaths: []string{"/ping"},
diff --git a/pkg/middleware/redirect_to_https_test.go b/pkg/middleware/redirect_to_https_test.go
index 6e8a4368..9c7fb751 100644
--- a/pkg/middleware/redirect_to_https_test.go
+++ b/pkg/middleware/redirect_to_https_test.go
@@ -6,6 +6,7 @@ import (
"net/http/httptest"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -39,6 +40,10 @@ var _ = Describe("RedirectToHTTPS suite", func() {
scope := &middlewareapi.RequestScope{
ReverseProxy: in.reverseProxy,
}
+ if in.reverseProxy {
+ req.RemoteAddr = "127.0.0.1:4180"
+ scope.TrustedProxies = newRedirectTrustedProxySet("127.0.0.1")
+ }
req = middlewareapi.AddRequestScope(req, scope)
rw := httptest.NewRecorder()
@@ -207,3 +212,13 @@ var _ = Describe("RedirectToHTTPS suite", func() {
}),
)
})
+
+func newRedirectTrustedProxySet(cidrs ...string) *ip.NetSet {
+ set := ip.NewNetSet()
+ for _, cidr := range cidrs {
+ ipNet := ip.ParseIPNet(cidr)
+ Expect(ipNet).ToNot(BeNil())
+ set.AddIPNet(*ipNet)
+ }
+ return set
+}
diff --git a/pkg/middleware/scope.go b/pkg/middleware/scope.go
index d0dd81ec..84b1573c 100644
--- a/pkg/middleware/scope.go
+++ b/pkg/middleware/scope.go
@@ -6,14 +6,16 @@ import (
"github.com/google/uuid"
"github.com/justinas/alice"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
)
-func NewScope(reverseProxy bool, idHeader string) alice.Constructor {
+func NewScope(reverseProxy bool, idHeader string, trustedProxies *ip.NetSet) alice.Constructor {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
scope := &middlewareapi.RequestScope{
- ReverseProxy: reverseProxy,
- RequestID: genRequestID(req, idHeader),
+ ReverseProxy: reverseProxy,
+ TrustedProxies: trustedProxies,
+ RequestID: genRequestID(req, idHeader),
}
req = middlewareapi.AddRequestScope(req, scope)
next.ServeHTTP(rw, req)
diff --git a/pkg/middleware/scope_test.go b/pkg/middleware/scope_test.go
index fa680667..db2c3016 100644
--- a/pkg/middleware/scope_test.go
+++ b/pkg/middleware/scope_test.go
@@ -6,6 +6,7 @@ import (
"github.com/google/uuid"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -32,7 +33,7 @@ var _ = Describe("Scope Suite", func() {
Context("ReverseProxy is false", func() {
BeforeEach(func() {
- handler := NewScope(false, testRequestHeader)(
+ handler := NewScope(false, testRequestHeader, nil)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextRequest = r
w.WriteHeader(200)
@@ -60,8 +61,15 @@ var _ = Describe("Scope Suite", func() {
})
Context("ReverseProxy is true", func() {
+ var trustedProxies *ip.NetSet
+
BeforeEach(func() {
- handler := NewScope(true, testRequestHeader)(
+ var err error
+
+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+
+ handler := NewScope(true, testRequestHeader, trustedProxies)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextRequest = r
w.WriteHeader(200)
@@ -74,12 +82,18 @@ var _ = Describe("Scope Suite", func() {
Expect(scope).ToNot(BeNil())
Expect(scope.ReverseProxy).To(BeTrue())
})
+
+ It("stores the trusted proxies on the scope", func() {
+ scope := middlewareapi.GetRequestScope(nextRequest)
+ Expect(scope).ToNot(BeNil())
+ Expect(scope.TrustedProxies).To(BeIdenticalTo(trustedProxies))
+ })
})
Context("Request ID header is present", func() {
BeforeEach(func() {
request.Header.Add(testRequestHeader, testRequestID)
- handler := NewScope(false, testRequestHeader)(
+ handler := NewScope(false, testRequestHeader, nil)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextRequest = r
w.WriteHeader(200)
@@ -97,7 +111,7 @@ var _ = Describe("Scope Suite", func() {
BeforeEach(func() {
uuid.SetRand(mockRand{})
- handler := NewScope(true, testRequestHeader)(
+ handler := NewScope(true, testRequestHeader, nil)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextRequest = r
w.WriteHeader(200)
diff --git a/pkg/middleware/stored_session.go b/pkg/middleware/stored_session.go
index 72c364e7..53238f19 100644
--- a/pkg/middleware/stored_session.go
+++ b/pkg/middleware/stored_session.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strings"
"time"
"github.com/justinas/alice"
@@ -31,6 +32,33 @@ const (
sessionRefreshRetryPeriod = 10 * time.Millisecond
)
+// isFatalRefreshError checks if a refresh error indicates a revoked or
+// non-existent session that should be immediately invalidated.
+// Fatal errors indicate the session is no longer valid at the provider level.
+// Non-fatal errors (network issues, timeouts) should not invalidate the session.
+//
+// Only checks standard OAuth2 error codes (RFC 6749 Section 5.2).
+// Does NOT check error_description strings as they are optional and provider-specific.
+func isFatalRefreshError(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ // Only check standard OAuth2 error codes (RFC 6749 Section 5.2)
+ // Do NOT check error_description strings as they are optional and provider-specific
+ fatalErrors := []string{
+ "invalid_grant", // refresh token revoked, expired, or session terminated
+ "invalid_client", // client credentials no longer valid
+ }
+
+ for _, fe := range fatalErrors {
+ if strings.Contains(err.Error(), fe) {
+ return true
+ }
+ }
+ return false
+}
+
// StoredSessionLoaderOptions contains all of the requirements to construct
// a stored session loader.
// All options must be provided.
@@ -188,9 +216,24 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
// We are holding the lock and the session needs a refresh
logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
if err := s.refreshSession(rw, req, session); err != nil {
- // If a preemptive refresh fails, we still keep the session
- // if validateSession succeeds.
logger.Errorf("Unable to refresh session: %v", err)
+
+ // Check if this is a fatal error that indicates the session is revoked
+ // or no longer valid at the provider level
+ if isFatalRefreshError(err) {
+ logger.Printf("Fatal refresh error detected (session revoked or invalid), clearing session for user: %s", session.User)
+
+ // Clear the session from storage (Redis) and remove the cookie
+ if err := s.store.Clear(rw, req); err != nil {
+ logger.Errorf("failed clearing session: %v", err)
+ }
+
+ // Return error immediately to force re-authentication
+ return fmt.Errorf("session invalidated due to fatal refresh error: %w", err)
+ }
+
+ // For non-fatal errors (network issues, timeouts), keep the session
+ // and let validateSession determine if it's still usable
}
// Validate all sessions after any Redeem/Refresh operation (fail or success)
diff --git a/pkg/middleware/stored_session_test.go b/pkg/middleware/stored_session_test.go
index d8e78f2f..c913a4ef 100644
--- a/pkg/middleware/stored_session_test.go
+++ b/pkg/middleware/stored_session_test.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"sync"
+ "testing"
"time"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -801,3 +802,53 @@ func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
return nil
}
+
+// TestIsFatalRefreshError tests the isFatalRefreshError function to ensure
+// it correctly identifies fatal OAuth2 errors that should invalidate a session.
+func TestIsFatalRefreshError(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ expected bool
+ }{
+ {
+ name: "nil error",
+ err: nil,
+ expected: false,
+ },
+ {
+ name: "invalid_grant error",
+ err: fmt.Errorf("failed to get token: oauth2: \"invalid_grant\" \"Session not active\""),
+ expected: true,
+ },
+ {
+ name: "invalid_client error",
+ err: fmt.Errorf("invalid_client: client not found"),
+ expected: true,
+ },
+ {
+ name: "network timeout - not fatal",
+ err: fmt.Errorf("Post \"https://keycloak/token\": dial tcp: connect: connection refused"),
+ expected: false,
+ },
+ {
+ name: "server error - not fatal",
+ err: fmt.Errorf("unexpected status code 500"),
+ expected: false,
+ },
+ {
+ name: "generic refresh error - not fatal",
+ err: fmt.Errorf("error refreshing tokens: context deadline exceeded"),
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := isFatalRefreshError(tt.err)
+ if result != tt.expected {
+ t.Errorf("isFatalRefreshError(%v) = %v, want %v", tt.err, result, tt.expected)
+ }
+ })
+ }
+}
diff --git a/pkg/requests/util/util.go b/pkg/requests/util/util.go
index 290f8059..568ebcc6 100644
--- a/pkg/requests/util/util.go
+++ b/pkg/requests/util/util.go
@@ -15,30 +15,30 @@ const (
)
// GetRequestProto returns the request scheme or X-Forwarded-Proto if present
-// and the request is proxied.
+// and the request came from a trusted reverse proxy.
func GetRequestProto(req *http.Request) string {
proto := req.Header.Get(XForwardedProto)
- if !IsProxied(req) || proto == "" {
+ if !CanTrustForwardedHeaders(req) || proto == "" {
proto = req.URL.Scheme
}
return proto
}
// GetRequestHost returns the request host header or X-Forwarded-Host if
-// present and the request is proxied.
+// present and the request came from a trusted reverse proxy.
func GetRequestHost(req *http.Request) string {
host := req.Header.Get(XForwardedHost)
- if !IsProxied(req) || host == "" {
+ if !CanTrustForwardedHeaders(req) || host == "" {
host = req.Host
}
return host
}
// GetRequestURI return the request URI or X-Forwarded-Uri if present and the
-// request is proxied.
+// request came from a trusted reverse proxy.
func GetRequestURI(req *http.Request) string {
uri := req.Header.Get(XForwardedURI)
- if !IsProxied(req) || uri == "" {
+ if !CanTrustForwardedHeaders(req) || uri == "" {
// Use RequestURI to preserve ?query
uri = req.URL.RequestURI()
}
@@ -46,17 +46,29 @@ func GetRequestURI(req *http.Request) string {
}
// GetRequestPath returns the request URI or X-Forwarded-Uri if present and the
-// request is proxied but always strips the query parameters and only returns
-// the pure path
+// request came from a trusted reverse proxy but always strips the query
+// parameters and fragment suffixes and only returns the pure path.
func GetRequestPath(req *http.Request) string {
- uri := GetRequestURI(req)
+ uri := stripRequestFragment(GetRequestURI(req))
// Parse URI and return only the path component
if parsedURL, err := url.Parse(uri); err == nil {
- return parsedURL.Path
+ return stripRequestFragment(parsedURL.Path)
}
// Fallback: strip query parameters manually
+ return stripRequestQuery(uri)
+}
+
+func stripRequestFragment(uri string) string {
+ if idx := strings.Index(uri, "#"); idx != -1 {
+ return uri[:idx]
+ }
+
+ return uri
+}
+
+func stripRequestQuery(uri string) string {
if idx := strings.Index(uri, "?"); idx != -1 {
return uri[:idx]
}
@@ -64,17 +76,18 @@ func GetRequestPath(req *http.Request) string {
return uri
}
-// IsProxied determines if a request was from a proxy based on the RequestScope
-// ReverseProxy tracker.
-func IsProxied(req *http.Request) bool {
+// CanTrustForwardedHeaders determines if forwarded headers should be processed
+// based on the RequestScope and the direct caller's address.
+func CanTrustForwardedHeaders(req *http.Request) bool {
scope := middlewareapi.GetRequestScope(req)
if scope == nil {
return false
}
- return scope.ReverseProxy
+
+ return scope.CanTrustForwardedHeaders(req)
}
func IsForwardedRequest(req *http.Request) bool {
- return IsProxied(req) &&
+ return CanTrustForwardedHeaders(req) &&
req.Host != GetRequestHost(req)
}
diff --git a/pkg/requests/util/util_test.go b/pkg/requests/util/util_test.go
index ba72c66d..c4185b35 100644
--- a/pkg/requests/util/util_test.go
+++ b/pkg/requests/util/util_test.go
@@ -6,6 +6,7 @@ import (
"net/http/httptest"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -19,8 +20,13 @@ var _ = Describe("Util Suite", func() {
uriNoQueryParams = "/test/endpoint"
)
var req *http.Request
+ var trustedProxies *ip.NetSet
BeforeEach(func() {
+ var err error
+ trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
+ Expect(err).ToNot(HaveOccurred())
+
req = httptest.NewRequest(
http.MethodGet,
fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
@@ -29,7 +35,7 @@ var _ = Describe("Util Suite", func() {
})
Context("GetRequestHost", func() {
- Context("IsProxied is false", func() {
+ Context("trusted forwarded headers are disabled", func() {
BeforeEach(func() {
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
})
@@ -44,10 +50,12 @@ var _ = Describe("Util Suite", func() {
})
})
- Context("IsProxied is true", func() {
+ Context("trusted forwarded headers are enabled", func() {
BeforeEach(func() {
+ req.RemoteAddr = "127.0.0.1:4180"
req = middleware.AddRequestScope(req, &middleware.RequestScope{
- ReverseProxy: true,
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
})
})
@@ -63,7 +71,7 @@ var _ = Describe("Util Suite", func() {
})
Context("GetRequestProto", func() {
- Context("IsProxied is false", func() {
+ Context("trusted forwarded headers are disabled", func() {
BeforeEach(func() {
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
})
@@ -78,10 +86,12 @@ var _ = Describe("Util Suite", func() {
})
})
- Context("IsProxied is true", func() {
+ Context("trusted forwarded headers are enabled", func() {
BeforeEach(func() {
+ req.RemoteAddr = "127.0.0.1:4180"
req = middleware.AddRequestScope(req, &middleware.RequestScope{
- ReverseProxy: true,
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
})
})
@@ -97,7 +107,7 @@ var _ = Describe("Util Suite", func() {
})
Context("GetRequestURI", func() {
- Context("IsProxied is false", func() {
+ Context("trusted forwarded headers are disabled", func() {
BeforeEach(func() {
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
})
@@ -112,10 +122,12 @@ var _ = Describe("Util Suite", func() {
})
})
- Context("IsProxied is true", func() {
+ Context("trusted forwarded headers are enabled", func() {
BeforeEach(func() {
+ req.RemoteAddr = "127.0.0.1:4180"
req = middleware.AddRequestScope(req, &middleware.RequestScope{
- ReverseProxy: true,
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
})
})
@@ -131,7 +143,7 @@ var _ = Describe("Util Suite", func() {
})
Context("GetRequestPath", func() {
- Context("IsProxied is false", func() {
+ Context("trusted forwarded headers are disabled", func() {
BeforeEach(func() {
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
})
@@ -140,16 +152,35 @@ var _ = Describe("Util Suite", func() {
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
})
+ It("drops fragment content from a parsed request path", func() {
+ // Simulate net/http ParseRequestURI preserving '#' in URL.Path.
+ req.URL.Path = "/foo/secret#/bar"
+ req.URL.RawPath = "/foo/secret%23/bar"
+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
+ })
+
+ It("drops fragment-like suffixes from encoded number signs", func() {
+ req = httptest.NewRequest(
+ http.MethodGet,
+ fmt.Sprintf("%s://%s/foo/secret%%23/bar?query=param", proto, host),
+ nil,
+ )
+ req = middleware.AddRequestScope(req, &middleware.RequestScope{})
+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
+ })
+
It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
})
})
- Context("IsProxied is true", func() {
+ Context("trusted forwarded headers are enabled", func() {
BeforeEach(func() {
+ req.RemoteAddr = "127.0.0.1:4180"
req = middleware.AddRequestScope(req, &middleware.RequestScope{
- ReverseProxy: true,
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
})
})
@@ -161,6 +192,37 @@ var _ = Describe("Util Suite", func() {
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
})
+
+ It("drops fragment-like suffixes from the X-Forwarded-Uri", func() {
+ req.Header.Add("X-Forwarded-Uri", "/foo/secret%23/bar?query=param")
+ Expect(util.GetRequestPath(req)).To(Equal("/foo/secret"))
+ })
+ })
+ })
+
+ Context("CanTrustForwardedHeaders", func() {
+ It("returns false when no scope is present", func() {
+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
+ })
+
+ It("returns true when the remote address is trusted", func() {
+ req.RemoteAddr = "127.0.0.1:4180"
+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
+ })
+
+ Expect(util.CanTrustForwardedHeaders(req)).To(BeTrue())
+ })
+
+ It("returns false when the remote address is untrusted", func() {
+ req.RemoteAddr = "192.0.2.10:4180"
+ req = middleware.AddRequestScope(req, &middleware.RequestScope{
+ ReverseProxy: true,
+ TrustedProxies: trustedProxies,
+ })
+
+ Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
})
})
})
diff --git a/pkg/upstream/http_test.go b/pkg/upstream/http_test.go
index a01d5c09..70af9e7e 100644
--- a/pkg/upstream/http_test.go
+++ b/pkg/upstream/http_test.go
@@ -498,7 +498,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
- proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
+ proxyServer = httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
})
AfterEach(func() {
@@ -549,7 +549,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
Expect(err).ToNot(HaveOccurred())
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
- noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
+ noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id", nil)(handler))
defer noPassHostServer.Close()
origin := "http://example.localhost"
diff --git a/pkg/validation/allowlist.go b/pkg/validation/allowlist.go
index a74f4ae9..dcc0d361 100644
--- a/pkg/validation/allowlist.go
+++ b/pkg/validation/allowlist.go
@@ -16,6 +16,7 @@ func validateAllowlists(o *options.Options) []string {
msgs = append(msgs, validateAuthRoutes(o)...)
msgs = append(msgs, validateAuthRegexes(o)...)
+ msgs = append(msgs, validateTrustedProxyIPs(o)...)
msgs = append(msgs, validateTrustedIPs(o)...)
if len(o.TrustedIPs) > 0 && o.ReverseProxy {
@@ -28,6 +29,17 @@ func validateAllowlists(o *options.Options) []string {
return msgs
}
+// validateTrustedProxyIPs validates IP/CIDRs for trusted reverse proxies.
+func validateTrustedProxyIPs(o *options.Options) []string {
+ msgs := []string{}
+ for i, ipStr := range o.TrustedProxyIPs {
+ if ip.ParseIPNet(ipStr) == nil {
+ msgs = append(msgs, fmt.Sprintf("trusted_proxy_ips[%d] (%s) could not be recognized", i, ipStr))
+ }
+ }
+ return msgs
+}
+
// validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes
func validateAuthRoutes(o *options.Options) []string {
msgs := []string{}
diff --git a/pkg/validation/allowlist_test.go b/pkg/validation/allowlist_test.go
index 9f6843dd..ae4c29ef 100644
--- a/pkg/validation/allowlist_test.go
+++ b/pkg/validation/allowlist_test.go
@@ -23,6 +23,11 @@ var _ = Describe("Allowlist", func() {
errStrings []string
}
+ type validateTrustedProxyIPsTableInput struct {
+ trustedProxyIPs []string
+ errStrings []string
+ }
+
DescribeTable("validateRoutes",
func(r *validateRoutesTableInput) {
opts := &options.Options{
@@ -121,4 +126,29 @@ var _ = Describe("Allowlist", func() {
},
}),
)
+
+ DescribeTable("validateTrustedProxyIPs",
+ func(t *validateTrustedProxyIPsTableInput) {
+ opts := &options.Options{
+ TrustedProxyIPs: t.trustedProxyIPs,
+ }
+ Expect(validateTrustedProxyIPs(opts)).To(ConsistOf(t.errStrings))
+ },
+ Entry("Valid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
+ trustedProxyIPs: []string{
+ "127.0.0.1",
+ "10.32.0.1/32",
+ "::1",
+ "2a12:105:ee7:9234:0:0:0:0/64",
+ },
+ errStrings: []string{},
+ }),
+ Entry("Invalid trusted proxy IPs", &validateTrustedProxyIPsTableInput{
+ trustedProxyIPs: []string{"[::1]", "alkwlkbn/32"},
+ errStrings: []string{
+ "trusted_proxy_ips[0] ([::1]) could not be recognized",
+ "trusted_proxy_ips[1] (alkwlkbn/32) could not be recognized",
+ },
+ }),
+ )
})
diff --git a/validator.go b/validator.go
index 587ef857..d03157a5 100644
--- a/validator.go
+++ b/validator.go
@@ -110,6 +110,10 @@ func NewValidator(domains []string, usersFile string) func(string) bool {
// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
func isEmailValidWithDomains(email string, allowedDomains []string) bool {
+ if strings.Count(email, "@") != 1 {
+ return false
+ }
+
for _, domain := range allowedDomains {
// allow if the domain is perfect suffix match with the email
if strings.HasSuffix(email, "@"+domain) {
@@ -119,7 +123,6 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
// allow if the domain is prefixed with . or *. and
// the last element (split on @) has the suffix as the domain
atoms := strings.Split(email, "@")
-
if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
(strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
return true
diff --git a/validator_test.go b/validator_test.go
index 976c0d7d..c406a732 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -404,6 +404,27 @@ func TestValidatorCases(t *testing.T) {
allowedDomains: []string{"*.company.com"},
expectedAuthZ: false,
},
+ {
+ name: "CheckThatTwoAtSignsIsInvalid",
+ email: "attacker@evil.com@company.com",
+ allowedEmails: []string(nil),
+ allowedDomains: []string{"company.com"},
+ expectedAuthZ: false,
+ },
+ {
+ name: "CheckThatTwoAtSignsIsInvalidEvenWithDotPrefix",
+ email: "attacker@evil.com@company.com",
+ allowedEmails: []string(nil),
+ allowedDomains: []string{".company.com"},
+ expectedAuthZ: false,
+ },
+ {
+ name: "CheckThatTwoAtSignsIsInvalidEvenWithWildcardPrefix",
+ email: "attacker@evil.com@foo.company.com",
+ allowedEmails: []string(nil),
+ allowedDomains: []string{"*.company.com"},
+ expectedAuthZ: false,
+ },
}
for _, tc := range testCases {