From 26de082a787225aabd4b18f56e1eeea9f676ae85 Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Sun, 12 Apr 2026 14:21:47 +0200 Subject: [PATCH 1/9] chore(deps): update gomod dependencies (#3411) - github.com/coreos/go-oidc/v3 v3.17.0 + github.com/coreos/go-oidc/v3 v3.18.0 - github.com/go-jose/go-jose/v3 v3.0.4 + github.com/go-jose/go-jose/v3 v3.0.5 - github.com/go-viper/mapstructure/v2 v2.4.0 + github.com/go-viper/mapstructure/v2 v2.5.0 - golang.org/x/crypto v0.49.0 + golang.org/x/crypto v0.50.0 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 - google.golang.org/api v0.272.0 + google.golang.org/api v0.275.0 --------- Signed-off-by: Jan Larwig --- CHANGELOG.md | 2 ++ go.mod | 40 +++++++++++++++--------------- go.sum | 46 +++++++++++++++++++++++++++++++++++ main_test.go | 4 +-- pkg/apis/options/load_test.go | 2 +- 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4c2379..beb1452b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ # V7.15.1 +- [#3411](https://github.com/oauth2-proxy/oauth2-proxy/pull/3411) chore(deps): update gomod dependencies (@tuunit) + ## Release Highlights - ๐Ÿ› Squashed some bugs 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/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", From 2e1261c4bec8f092a42df3ac57ae54ea2acf0142 Mon Sep 17 00:00:00 2001 From: Francesco Pasqualini Date: Sun, 12 Apr 2026 14:48:55 +0200 Subject: [PATCH 2/9] fix: invalidate session on fatal OAuth2 refresh errors (#3333) * fix: invalidate session on fatal OAuth2 refresh errors When a token refresh fails with a fatal OAuth2 error (invalid_grant, invalid_client), the session is now cleared from the session store and the cookie is removed, forcing re-authentication. Previously, fatal refresh errors were logged but the stale session continued to be served, leaving users logged in indefinitely after their session was revoked at the provider level. Transient errors (network timeouts, server errors) continue to preserve the existing session as before. Fixes #1945 Signed-off-by: Francesco Pasqualini * fix: apply review nits and add CHANGELOG entry Signed-off-by: Francesco Pasqualini Signed-off-by: Jan Larwig --------- Signed-off-by: Francesco Pasqualini Signed-off-by: Jan Larwig --- CHANGELOG.md | 5 +-- pkg/middleware/stored_session.go | 47 ++++++++++++++++++++++-- pkg/middleware/stored_session_test.go | 51 +++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb1452b..e8f2d777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,10 @@ ## Changes since v7.15.1 -# V7.15.1 - - [#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) + +# V7.15.1 ## Release Highlights 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) + } + }) + } +} From 0337a95fc6bb4c2798e555b28ae61500b30a6b9b Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Mon, 13 Apr 2026 18:17:50 +0200 Subject: [PATCH 3/9] Merge commit from fork * fix: clear session cookie at beginning of signinpage handler Co-authored-by: Christopher Schrewing Signed-off-by: Michael Bella Signed-off-by: Jan Larwig * test: clear session cookie at beginning of signinpage handler Signed-off-by: Jan Larwig * doc: changelog entry for GHSA-f24x-5g9q-753f Signed-off-by: Jan Larwig --------- Signed-off-by: Michael Bella Signed-off-by: Jan Larwig Co-authored-by: Christopher Schrewing --- CHANGELOG.md | 1 + oauthproxy.go | 8 ++++---- oauthproxy_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f2d777..2dccaaf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - [#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) # V7.15.1 diff --git a/oauthproxy.go b/oauthproxy.go index 3efe66fd..0685bbbb 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -634,6 +634,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 +651,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..46e39a90 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 { From 43596a7bab2053e091ddc513c865e20b4e4b08ea Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Mon, 13 Apr 2026 18:20:36 +0200 Subject: [PATCH 4/9] Merge commit from fork Signed-off-by: Jan Larwig --- CHANGELOG.md | 1 + pkg/middleware/healthcheck.go | 11 +++++--- pkg/middleware/healthcheck_test.go | 40 +++++++++++++++++++++++------- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dccaaf1..3e1dc347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - [#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) # V7.15.1 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"}, From aff369dfa31ca6b8166b4e14613e8fb2d7dac88a Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Mon, 13 Apr 2026 18:22:56 +0200 Subject: [PATCH 5/9] Merge commit from fork Signed-off-by: Jan Larwig --- CHANGELOG.md | 1 + .../docker-compose-nginx.yaml | 5 +- contrib/local-environment/nginx.conf | 54 +------- .../local-environment/oauth2-proxy-nginx.cfg | 17 ++- docs/docs/configuration/overview.md | 5 + .../version-7.15.x/configuration/overview.md | 5 + oauthproxy.go | 33 +++-- oauthproxy_test.go | 38 ++++++ pkg/apis/middleware/scope.go | 49 +++++++- pkg/apis/middleware/scope_test.go | 34 +++++ pkg/apis/options/options.go | 2 + pkg/app/redirect/director_test.go | 35 +++++- pkg/cookies/cookies_test.go | 8 +- pkg/ip/parse_ip_net.go | 16 +++ pkg/ip/parse_ip_net_test.go | 118 ++++++++++++++++++ pkg/middleware/redirect_to_https_test.go | 15 +++ pkg/middleware/scope.go | 8 +- pkg/middleware/scope_test.go | 22 +++- pkg/requests/util/util.go | 27 ++-- pkg/requests/util/util_test.go | 64 ++++++++-- pkg/upstream/http_test.go | 4 +- pkg/validation/allowlist.go | 12 ++ pkg/validation/allowlist_test.go | 30 +++++ 23 files changed, 498 insertions(+), 104 deletions(-) create mode 100644 pkg/ip/parse_ip_net_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1dc347..8be9736e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - [#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) # V7.15.1 diff --git a/contrib/local-environment/docker-compose-nginx.yaml b/contrib/local-environment/docker-compose-nginx.yaml index ed93d57c..23138eb4 100644 --- a/contrib/local-environment/docker-compose-nginx.yaml +++ b/contrib/local-environment/docker-compose-nginx.yaml @@ -23,7 +23,8 @@ version: "3.0" services: oauth2-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1 - ports: [] + ports: + - 4180:4180/tcp hostname: oauth2-proxy container_name: oauth2-proxy command: --config /oauth2-proxy.cfg @@ -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/nginx.conf b/contrib/local-environment/nginx.conf index 15005bf6..f3761387 100644 --- a/contrib/local-environment/nginx.conf +++ b/contrib/local-environment/nginx.conf @@ -1,11 +1,12 @@ # Reverse proxy to oauth2-proxy server { - listen 80; - server_name oauth2-proxy.oauth2-proxy.localhost; + listen 8080; + server_name oauth2-proxy.localtest.me; location / { 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/; } @@ -13,8 +14,8 @@ server { # Reverse proxy to httpbin server { - listen 80; - server_name httpbin.oauth2-proxy.localhost; + listen 8080; + server_name httpbin.localtest.me; auth_request /internal-auth/oauth2/auth; @@ -29,50 +30,7 @@ server { # 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 - error_page 401 = @oauth2_signin; - - root /usr/share/nginx/html; - index index.html index.htm; - } - - # 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; - } - - # 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; + return 302 http://oauth2-proxy.localtest.me:8080/oauth2/sign_in?rd=$scheme://$host$request_uri; } # auth_request must be a URI so this allows an internal path to then proxy to diff --git a/contrib/local-environment/oauth2-proxy-nginx.cfg b/contrib/local-environment/oauth2-proxy-nginx.cfg index 01b64a55..0a383ab7 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:4180/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..b145065a 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,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/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/oauthproxy.go b/oauthproxy.go index 0685bbbb..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() diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 46e39a90..df5912a0 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -2843,6 +2843,7 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { 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() @@ -2857,6 +2858,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) 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/options.go b/pkg/apis/options/options.go index b57d5aed..ac2b13c8 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/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/requests/util/util.go b/pkg/requests/util/util.go index 290f8059..7b9b4919 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,8 +46,8 @@ 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 only returns the pure path. func GetRequestPath(req *http.Request) string { uri := GetRequestURI(req) @@ -64,17 +64,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..ed7a88a6 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{}) }) @@ -146,10 +158,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, }) }) @@ -163,4 +177,30 @@ var _ = Describe("Util Suite", func() { }) }) }) + + 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", + }, + }), + ) }) From cc0e0335ea9e33c0220c7ce1181e5d41dcac7b48 Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Mon, 13 Apr 2026 18:24:51 +0200 Subject: [PATCH 6/9] Merge commit from fork Signed-off-by: Jan Larwig --- CHANGELOG.md | 1 + oauthproxy_test.go | 18 ++++++++++++++++++ validator.go | 5 ++++- validator_test.go | 21 +++++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be9736e..bfce323a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [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-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/oauthproxy_test.go b/oauthproxy_test.go index df5912a0..fc9c34bd 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -3486,6 +3486,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/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 { From bdfde725c6175a5b10e8b5ffe9f09159ea2db03c Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Mon, 13 Apr 2026 18:29:01 +0200 Subject: [PATCH 7/9] Merge commit from fork Signed-off-by: Jan Larwig --- CHANGELOG.md | 1 + docs/docs/configuration/overview.md | 4 +-- oauthproxy_test.go | 52 +++++++++++++++++++++++++++++ pkg/requests/util/util.go | 18 ++++++++-- pkg/requests/util/util_test.go | 22 ++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfce323a..9f227932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - [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-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) +- [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) # V7.15.1 diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index b145065a..965953fa 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -218,8 +218,8 @@ When `--reverse-proxy` is enabled, configure `--trusted-proxy-ip` to the IPs or | 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/oauthproxy_test.go b/oauthproxy_test.go index fc9c34bd..e1235a4e 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -2679,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) @@ -2714,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", @@ -2738,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 { @@ -2778,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) @@ -2813,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", @@ -2837,6 +2877,18 @@ 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 { diff --git a/pkg/requests/util/util.go b/pkg/requests/util/util.go index 7b9b4919..568ebcc6 100644 --- a/pkg/requests/util/util.go +++ b/pkg/requests/util/util.go @@ -47,16 +47,28 @@ func GetRequestURI(req *http.Request) string { // GetRequestPath returns the request URI or X-Forwarded-Uri if present and the // request came from a trusted reverse proxy but always strips the query -// parameters and only returns the pure path. +// 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] } diff --git a/pkg/requests/util/util_test.go b/pkg/requests/util/util_test.go index ed7a88a6..c4185b35 100644 --- a/pkg/requests/util/util_test.go +++ b/pkg/requests/util/util_test.go @@ -152,6 +152,23 @@ 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)) @@ -175,6 +192,11 @@ 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")) + }) }) }) From 5961fd99b42c3625b8ef08690d38be5cb37f44b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:12:28 +0200 Subject: [PATCH 8/9] release v7.15.2 (#3413) * update to release version v7.15.2 * doc: add changelog entry for v7.15.2 Signed-off-by: Jan Larwig * fix(deps): override webpackbar to v7 for webpack 5.106.0 compatibility As outlined in https://github.com/facebook/docusaurus/issues/11923 Signed-off-by: Jan Larwig * chore: fix local test files for nginx setup Signed-off-by: Jan Larwig --------- Signed-off-by: Jan Larwig Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jan Larwig --- CHANGELOG.md | 44 +++++++++++- contrib/local-environment/dex.yaml | 27 ++++---- .../docker-compose-alpha-config.yaml | 2 +- .../docker-compose-gitea.yaml | 2 +- .../docker-compose-keycloak.yaml | 10 +-- .../docker-compose-nginx.yaml | 6 +- .../docker-compose-traefik.yaml | 2 +- contrib/local-environment/docker-compose.yaml | 2 +- contrib/local-environment/nginx.conf | 67 +++++++++---------- .../local-environment/oauth2-proxy-nginx.cfg | 2 +- docs/docs/installation.md | 2 +- docs/package.json | 3 + .../version-7.15.x/installation.md | 2 +- 13 files changed, 106 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f227932..320ba697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,48 @@ ## 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 - [#3411](https://github.com/oauth2-proxy/oauth2-proxy/pull/3411) chore(deps): update gomod dependencies (@tuunit) @@ -13,8 +55,8 @@ - [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-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) - [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/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 23138eb4..2aa403ec 100644 --- a/contrib/local-environment/docker-compose-nginx.yaml +++ b/contrib/local-environment/docker-compose-nginx.yaml @@ -22,12 +22,12 @@ version: "3.0" services: oauth2-proxy: - image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.1 - ports: - - 4180:4180/tcp + 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: 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 f3761387..0e7bf7b4 100644 --- a/contrib/local-environment/nginx.conf +++ b/contrib/local-environment/nginx.conf @@ -1,49 +1,44 @@ -# Reverse proxy to oauth2-proxy -server { - listen 8080; - server_name oauth2-proxy.localtest.me; - - location / { - 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/; - } -} - # Reverse proxy to httpbin server { listen 8080; - server_name httpbin.localtest.me; + 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 / { + auth_request /oauth2/auth; + error_page 401 = @oauth2_signin; + + # 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.localtest.me:8080/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/; + 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 0a383ab7..2565c226 100644 --- a/contrib/local-environment/oauth2-proxy-nginx.cfg +++ b/contrib/local-environment/oauth2-proxy-nginx.cfg @@ -9,7 +9,7 @@ whitelist_domains=[".localtest.me"] # Required to allow redirection back to orig # dex provider client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" client_id="oauth2-proxy" -redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback" +redirect_url="http://oauth2-proxy.localtest.me:8080/oauth2/callback" oidc_issuer_url="http://dex.localtest.me:5556/dex" provider="oidc" 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/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 From 65037b086c783903a8dfc31a6f16a995ff8cf007 Mon Sep 17 00:00:00 2001 From: Jan Larwig Date: Fri, 17 Apr 2026 10:56:42 +0200 Subject: [PATCH 9/9] change affiliation --- MAINTAINERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 |