git.iko.soy/typeable/oauth2-proxy
on master help sign in
Strip X-Forwarded auth headers from whitelisted paths (#624)

* Strip X-Forwarded auth headers from whitelisted paths For any paths that match skip-auth-regex, strip normal X-Forwarded headers that would be sent based on pass-user-headers or pass-access-token settings. This prevents malicious injecting of authentication headers through the skip-auth-regex paths in cases where the regex might be misconfigured and too open. Control this behavior with --skip-auth-strip-headers flag. This flag is set to TRUE by default (this is secure by default, but potentially breaks some legacy configurations). Only x-Forwarded headers stripped, left the Authorization header untouched. * Strip authorization header if it would be set * Improve TestStripAuthHeaders test table * Improve --skip-auth-strip-headers flag documentation

commitabeb0236d87969243a6b061c5a601c8daffc424c authorNick Meves <nick.meves@greenhouse.io> dateTue 14 Jul 2020, 15:46 (6 years ago) parentbb59770
5 files changed+176−1
CHANGELOG.md+1 docs/configuration/configuration.md+1 oauthproxy.go+35−1 oauthproxy_test.go+136 pkg/apis/options/options.go+3
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111
1212## Changes since v6.0.0
1313
14+- [#624](https://github.com/oauth2-proxy/oauth2-proxy/pull/624) Allow stripping authentication headers from whitelisted requests with `--skip-auth-strip-headers` (@NickMeves)
1415- [#673](https://github.com/oauth2-proxy/oauth2-proxy/pull/673) Add --session-cookie-minimal option to create session cookies with no tokens (@NickMeves)
1516- [#632](https://github.com/oauth2-proxy/oauth2-proxy/pull/632) Reduce session size by encoding with MessagePack and using LZ4 compression (@NickMeves)
1617- [#675](https://github.com/oauth2-proxy/oauth2-proxy/pull/675) Fix required ruby version and deprecated option for building docs (@mkontani)
docs/configuration/configuration.md+1
@@ -116,6 +116,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
116116| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
117117| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
118118| `--skip-auth-regex` | string | bypass authentication for requests paths that match (may be given multiple times) | |
119+| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy for request paths in `--skip-auth-regex` | false |
119120| `--skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens | false |
120121| `--skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `--login-url`, `--redeem-url` and `--oidc-jwks-url` must be configured in this case | false |
121122| `--skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false |
oauthproxy.go+35−1
@@ -111,6 +111,7 @@ type OAuthProxy struct {
111111 PreferEmailToUser bool
112112 skipAuthRegex []string
113113 skipAuthPreflight bool
114+ skipAuthStripHeaders bool
114115 skipJwtBearerTokens bool
115116 mainJwtBearerVerifier *oidc.IDTokenVerifier
116117 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
@@ -343,6 +344,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
343344 whitelistDomains: opts.WhitelistDomains,
344345 skipAuthRegex: opts.SkipAuthRegex,
345346 skipAuthPreflight: opts.SkipAuthPreflight,
347+ skipAuthStripHeaders: opts.SkipAuthStripHeaders,
346348 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
347349 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
348350 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
@@ -718,7 +720,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
718720 case path == p.RobotsPath:
719721 p.RobotsTxt(rw)
720722 case p.IsWhitelistedRequest(req):
721− p.serveMux.ServeHTTP(rw, req)
723+ p.SkipAuthProxy(rw, req)
722724 case path == p.SignInPath:
723725 p.SignIn(rw, req)
724726 case path == p.SignOutPath:
@@ -891,6 +893,14 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
891893 rw.WriteHeader(http.StatusAccepted)
892894}
893895
896+// SkipAuthProxy proxies whitelisted requests and skips authentication
897+func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
898+ if p.skipAuthStripHeaders {
899+ p.stripAuthHeaders(req)
900+ }
901+ p.serveMux.ServeHTTP(rw, req)
902+}
903+
894904// Proxy proxies the user request if the user is authenticated else it prompts
895905// them to authenticate
896906func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
@@ -1122,6 +1132,30 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
11221132 }
11231133}
11241134
1135+// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
1136+func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
1137+ if p.PassBasicAuth {
1138+ req.Header.Del("X-Forwarded-User")
1139+ req.Header.Del("X-Forwarded-Email")
1140+ req.Header.Del("X-Forwarded-Preferred-Username")
1141+ req.Header.Del("Authorization")
1142+ }
1143+
1144+ if p.PassUserHeaders {
1145+ req.Header.Del("X-Forwarded-User")
1146+ req.Header.Del("X-Forwarded-Email")
1147+ req.Header.Del("X-Forwarded-Preferred-Username")
1148+ }
1149+
1150+ if p.PassAccessToken {
1151+ req.Header.Del("X-Forwarded-Access-Token")
1152+ }
1153+
1154+ if p.PassAuthorization {
1155+ req.Header.Del("Authorization")
1156+ }
1157+}
1158+
11251159// CheckBasicAuth checks the requests Authorization header for basic auth
11261160// credentials and authenticates these against the proxies HtpasswdFile
11271161func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
oauthproxy_test.go+136
@@ -723,6 +723,142 @@ func TestPassUserHeadersWithEmail(t *testing.T) {
723723 }
724724}
725725
726+func TestStripAuthHeaders(t *testing.T) {
727+ testCases := map[string]struct {
728+ SkipAuthStripHeaders bool
729+ PassBasicAuth bool
730+ PassUserHeaders bool
731+ PassAccessToken bool
732+ PassAuthorization bool
733+ StrippedHeaders map[string]bool
734+ }{
735+ "Default options": {
736+ SkipAuthStripHeaders: true,
737+ PassBasicAuth: true,
738+ PassUserHeaders: true,
739+ PassAccessToken: false,
740+ PassAuthorization: false,
741+ StrippedHeaders: map[string]bool{
742+ "X-Forwarded-User": true,
743+ "X-Forwarded-Email": true,
744+ "X-Forwarded-Preferred-Username": true,
745+ "X-Forwarded-Access-Token": false,
746+ "Authorization": true,
747+ },
748+ },
749+ "Pass access token": {
750+ SkipAuthStripHeaders: true,
751+ PassBasicAuth: true,
752+ PassUserHeaders: true,
753+ PassAccessToken: true,
754+ PassAuthorization: false,
755+ StrippedHeaders: map[string]bool{
756+ "X-Forwarded-User": true,
757+ "X-Forwarded-Email": true,
758+ "X-Forwarded-Preferred-Username": true,
759+ "X-Forwarded-Access-Token": true,
760+ "Authorization": true,
761+ },
762+ },
763+ "Nothing setting Authorization": {
764+ SkipAuthStripHeaders: true,
765+ PassBasicAuth: false,
766+ PassUserHeaders: true,
767+ PassAccessToken: true,
768+ PassAuthorization: false,
769+ StrippedHeaders: map[string]bool{
770+ "X-Forwarded-User": true,
771+ "X-Forwarded-Email": true,
772+ "X-Forwarded-Preferred-Username": true,
773+ "X-Forwarded-Access-Token": true,
774+ "Authorization": false,
775+ },
776+ },
777+ "Only Authorization header modified": {
778+ SkipAuthStripHeaders: true,
779+ PassBasicAuth: false,
780+ PassUserHeaders: false,
781+ PassAccessToken: false,
782+ PassAuthorization: true,
783+ StrippedHeaders: map[string]bool{
784+ "X-Forwarded-User": false,
785+ "X-Forwarded-Email": false,
786+ "X-Forwarded-Preferred-Username": false,
787+ "X-Forwarded-Access-Token": false,
788+ "Authorization": true,
789+ },
790+ },
791+ "Don't strip any headers (default options)": {
792+ SkipAuthStripHeaders: false,
793+ PassBasicAuth: true,
794+ PassUserHeaders: true,
795+ PassAccessToken: false,
796+ PassAuthorization: false,
797+ StrippedHeaders: map[string]bool{
798+ "X-Forwarded-User": false,
799+ "X-Forwarded-Email": false,
800+ "X-Forwarded-Preferred-Username": false,
801+ "X-Forwarded-Access-Token": false,
802+ "Authorization": false,
803+ },
804+ },
805+ "Don't strip any headers (custom options)": {
806+ SkipAuthStripHeaders: false,
807+ PassBasicAuth: true,
808+ PassUserHeaders: true,
809+ PassAccessToken: true,
810+ PassAuthorization: false,
811+ StrippedHeaders: map[string]bool{
812+ "X-Forwarded-User": false,
813+ "X-Forwarded-Email": false,
814+ "X-Forwarded-Preferred-Username": false,
815+ "X-Forwarded-Access-Token": false,
816+ "Authorization": false,
817+ },
818+ },
819+ }
820+
821+ initialHeaders := map[string]string{
822+ "X-Forwarded-User": "9fcab5c9b889a557",
823+ "X-Forwarded-Email": "john.doe@example.com",
824+ "X-Forwarded-Preferred-Username": "john.doe",
825+ "X-Forwarded-Access-Token": "AccessToken",
826+ "Authorization": "bearer IDToken",
827+ }
828+
829+ for name, tc := range testCases {
830+ t.Run(name, func(t *testing.T) {
831+ opts := baseTestOptions()
832+ opts.SkipAuthStripHeaders = tc.SkipAuthStripHeaders
833+ opts.PassBasicAuth = tc.PassBasicAuth
834+ opts.PassUserHeaders = tc.PassUserHeaders
835+ opts.PassAccessToken = tc.PassAccessToken
836+ opts.PassAuthorization = tc.PassAuthorization
837+ err := validation.Validate(opts)
838+ assert.NoError(t, err)
839+
840+ req, _ := http.NewRequest("GET", fmt.Sprintf("%s/testCase", opts.ProxyPrefix), nil)
841+ for header, val := range initialHeaders {
842+ req.Header.Set(header, val)
843+ }
844+
845+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
846+ assert.NoError(t, err)
847+ if proxy.skipAuthStripHeaders {
848+ proxy.stripAuthHeaders(req)
849+ }
850+
851+ for header, stripped := range tc.StrippedHeaders {
852+ if stripped {
853+ assert.Equal(t, req.Header.Get(header), "")
854+ } else {
855+ assert.Equal(t, req.Header.Get(header), initialHeaders[header])
856+ }
857+ }
858+ })
859+ }
860+}
861+
726862type PassAccessTokenTest struct {
727863 providerServer *httptest.Server
728864 proxy *OAuthProxy
pkg/apis/options/options.go+3
@@ -66,6 +66,7 @@ type Options struct {
6666
6767 Upstreams []string `flag:"upstream" cfg:"upstreams"`
6868 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
69+ SkipAuthStripHeaders bool `flag:"skip-auth-strip-headers" cfg:"skip_auth_strip_headers"`
6970 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
7071 ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"`
7172 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
@@ -159,6 +160,7 @@ func NewOptions() *Options {
159160 AzureTenant: "common",
160161 SetXAuthRequest: false,
161162 SkipAuthPreflight: false,
163+ SkipAuthStripHeaders: false,
162164 FlushInterval: time.Duration(1) * time.Second,
163165 PassBasicAuth: true,
164166 SetBasicAuth: false,
@@ -202,6 +204,7 @@ func NewFlagSet() *pflag.FlagSet {
202204 flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream")
203205 flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)")
204206 flagSet.StringSlice("skip-auth-regex", []string{}, "bypass authentication for requests path's that match (may be given multiple times)")
207+ flagSet.Bool("skip-auth-strip-headers", false, "strips X-Forwarded-* style authentication headers & Authorization header if they would be set by oauth2-proxy for request paths in --skip-auth-regex")
205208 flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
206209 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
207210 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")