providers/proxy: fix URL path getting lost when partial URL is given to rd= (#11354)
* providers/proxy: fix URL path getting lost when partial URL is given to rd= Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better fallback + tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -193,7 +193,17 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) { | 	mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		a.handleAuthStart(w, r, "") | 		fwd := "" | ||||||
|  | 		// This should only really be hit for nginx forward_auth | ||||||
|  | 		// as for that the auth start redirect URL is generated by the | ||||||
|  | 		// reverse proxy, and as such we won't have a request we just | ||||||
|  | 		// denied to reference for final URL | ||||||
|  | 		rd, ok := a.checkRedirectParam(r) | ||||||
|  | 		if ok { | ||||||
|  | 			a.log.WithField("rd", rd).Trace("Setting redirect") | ||||||
|  | 			fwd = rd | ||||||
|  | 		} | ||||||
|  | 		a.handleAuthStart(w, r, fwd) | ||||||
| 	}) | 	}) | ||||||
| 	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback) | 	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback) | ||||||
| 	mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut) | 	mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut) | ||||||
|  | |||||||
| @ -15,36 +15,6 @@ const ( | |||||||
| 	LogoutSignature   = "X-authentik-logout" | 	LogoutSignature   = "X-authentik-logout" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (a *Application) checkRedirectParam(r *http.Request) (string, bool) { |  | ||||||
| 	rd := r.URL.Query().Get(redirectParam) |  | ||||||
| 	if rd == "" { |  | ||||||
| 		return "", false |  | ||||||
| 	} |  | ||||||
| 	u, err := url.Parse(rd) |  | ||||||
| 	if err != nil { |  | ||||||
| 		a.log.WithError(err).Warning("Failed to parse redirect URL") |  | ||||||
| 		return "", false |  | ||||||
| 	} |  | ||||||
| 	// Check to make sure we only redirect to allowed places |  | ||||||
| 	if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE { |  | ||||||
| 		ext, err := url.Parse(a.proxyConfig.ExternalHost) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return "", false |  | ||||||
| 		} |  | ||||||
| 		ext.Scheme = "" |  | ||||||
| 		if !strings.Contains(u.String(), ext.String()) { |  | ||||||
| 			a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host") |  | ||||||
| 			return "", false |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) { |  | ||||||
| 			a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain") |  | ||||||
| 			return "", false |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return u.String(), true |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) { | func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) { | ||||||
| 	state, err := a.createState(r, fwd) | 	state, err := a.createState(r, fwd) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | |||||||
| @ -5,10 +5,13 @@ import ( | |||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/golang-jwt/jwt/v5" | 	"github.com/golang-jwt/jwt/v5" | ||||||
| 	"github.com/gorilla/securecookie" | 	"github.com/gorilla/securecookie" | ||||||
| 	"github.com/mitchellh/mapstructure" | 	"github.com/mitchellh/mapstructure" | ||||||
|  | 	"goauthentik.io/api/v3" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type OAuthState struct { | type OAuthState struct { | ||||||
| @ -27,6 +30,44 @@ func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error)       { return ni | |||||||
|  |  | ||||||
| var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) | var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) | ||||||
|  |  | ||||||
|  | // Validate that the given redirect parameter (?rd=...) is valid and can be used | ||||||
|  | // For proxy/forward_single this checks that if the `rd` param has a Hostname (and is a full URL) | ||||||
|  | // the hostname matches what's configured, or no hostname must be given | ||||||
|  | // For forward_domain this checks if the domain of the URL in `rd` ends with the configured domain | ||||||
|  | func (a *Application) checkRedirectParam(r *http.Request) (string, bool) { | ||||||
|  | 	rd := r.URL.Query().Get(redirectParam) | ||||||
|  | 	if rd == "" { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	u, err := url.Parse(rd) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("Failed to parse redirect URL") | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	// Check to make sure we only redirect to allowed places | ||||||
|  | 	if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE { | ||||||
|  | 		ext, err := url.Parse(a.proxyConfig.ExternalHost) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", false | ||||||
|  | 		} | ||||||
|  | 		// Either hostname needs to match the configured domain, or host name must be empty for just a path | ||||||
|  | 		if u.Host == "" { | ||||||
|  | 			u.Host = ext.Host | ||||||
|  | 			u.Scheme = ext.Scheme | ||||||
|  | 		} | ||||||
|  | 		if u.Host != ext.Host { | ||||||
|  | 			a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host") | ||||||
|  | 			return "", false | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) { | ||||||
|  | 			a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain") | ||||||
|  | 			return "", false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return u.String(), true | ||||||
|  | } | ||||||
|  |  | ||||||
| func (a *Application) createState(r *http.Request, fwd string) (string, error) { | func (a *Application) createState(r *http.Request, fwd string) (string, error) { | ||||||
| 	s, _ := a.sessions.Get(r, a.SessionName()) | 	s, _ := a.sessions.Get(r, a.SessionName()) | ||||||
| 	if s.ID == "" { | 	if s.ID == "" { | ||||||
| @ -39,17 +80,6 @@ func (a *Application) createState(r *http.Request, fwd string) (string, error) { | |||||||
| 		SessionID: s.ID, | 		SessionID: s.ID, | ||||||
| 		Redirect:  fwd, | 		Redirect:  fwd, | ||||||
| 	} | 	} | ||||||
| 	if fwd == "" { |  | ||||||
| 		// This should only really be hit for nginx forward_auth |  | ||||||
| 		// as for that the auth start redirect URL is generated by the |  | ||||||
| 		// reverse proxy, and as such we won't have a request we just |  | ||||||
| 		// denied to reference for final URL |  | ||||||
| 		rd, ok := a.checkRedirectParam(r) |  | ||||||
| 		if ok { |  | ||||||
| 			a.log.WithField("rd", rd).Trace("Setting redirect") |  | ||||||
| 			st.Redirect = rd |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, st) | 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, st) | ||||||
| 	tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret())) | 	tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret())) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | |||||||
| @ -8,25 +8,45 @@ import ( | |||||||
| 	"goauthentik.io/api/v3" | 	"goauthentik.io/api/v3" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestCheckRedirectParam(t *testing.T) { | func TestCheckRedirectParam_None(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
|  | 	// Test no rd param | ||||||
| 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil) | ||||||
|  |  | ||||||
| 	rd, ok := a.checkRedirectParam(req) | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
| 	assert.Equal(t, false, ok) | 	assert.Equal(t, false, ok) | ||||||
| 	assert.Equal(t, "", rd) | 	assert.Equal(t, "", rd) | ||||||
|  | } | ||||||
|  |  | ||||||
| 	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil) | func TestCheckRedirectParam_Invalid(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	// Test invalid rd param | ||||||
|  | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil) | ||||||
|  |  | ||||||
| 	rd, ok = a.checkRedirectParam(req) | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
| 	assert.Equal(t, false, ok) | 	assert.Equal(t, false, ok) | ||||||
| 	assert.Equal(t, "", rd) | 	assert.Equal(t, "", rd) | ||||||
|  | } | ||||||
|  |  | ||||||
| 	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil) | func TestCheckRedirectParam_ValidFull(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	// Test valid full rd param | ||||||
|  | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil) | ||||||
|  |  | ||||||
| 	rd, ok = a.checkRedirectParam(req) | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, true, ok) | ||||||
|  | 	assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCheckRedirectParam_ValidPartial(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	// Test valid partial rd param | ||||||
|  | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=/test?foo", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
| 	assert.Equal(t, true, ok) | 	assert.Equal(t, true, ok) | ||||||
| 	assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd) | 	assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L.
					Jens L.