Skip to content

Commit 942bca3

Browse files
committed
Avoid passing redirect_base parameters to URL
1 parent 0879962 commit 942bca3

1 file changed

Lines changed: 74 additions & 35 deletions

File tree

proxy/auth/redirect.go

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ import (
1313

1414
const oauthRedirectURICookiePrefix = "oauth_redirect_uri_"
1515

16-
// oauthCallbackFromOrigin builds the OAuth redirect_uri (…/callback) for the given UI origin
17-
// (scheme + host, no path). The path prefix comes from config.BaseUiUrl so deployments served
18-
// from a subpath (e.g. https://host/ui) resolve to …/ui/callback instead of …/callback.
19-
func oauthCallbackFromOrigin(origin *url.URL) (string, error) {
16+
// oauthCallbackURL builds the OAuth redirect_uri (…/callback) for the given UI origin scheme and host.
17+
// The path prefix comes from config.BaseUiUrl so deployments served from a subpath (e.g. https://host/ui)
18+
// resolve to …/ui/callback instead of …/callback.
19+
func oauthCallbackURL(scheme, host string) (string, error) {
20+
scheme = strings.ToLower(strings.TrimSpace(scheme))
21+
if scheme != "http" && scheme != "https" {
22+
return "", fmt.Errorf("invalid origin scheme")
23+
}
24+
host = strings.TrimSpace(host)
25+
if host == "" || strings.ContainsAny(host, "@\r\n\t") {
26+
return "", fmt.Errorf("invalid origin host")
27+
}
2028
baseUI, err := url.Parse(config.BaseUiUrl)
2129
if err != nil {
2230
return "", fmt.Errorf("invalid BASE_UI_URL configuration: %w", err)
@@ -26,12 +34,9 @@ func oauthCallbackFromOrigin(origin *url.URL) (string, error) {
2634
if basePath != "" {
2735
callbackPath = basePath + "/callback"
2836
}
29-
out := &url.URL{
30-
Scheme: origin.Scheme,
31-
Host: origin.Host,
32-
Path: callbackPath,
33-
}
34-
return out.String(), nil
37+
38+
pathURL := &url.URL{Path: callbackPath}
39+
return scheme + "://" + host + pathURL.EscapedPath(), nil
3540
}
3641

3742
// ResolveOAuthRedirectURI returns the OAuth redirect_uri (callback URL) for this login attempt.
@@ -47,27 +52,22 @@ func ResolveOAuthRedirectURI(r *http.Request, redirectBase string) (string, erro
4752
if err != nil {
4853
return "", fmt.Errorf("invalid BASE_UI_URL configuration: %w", err)
4954
}
50-
origin := &url.URL{Scheme: baseUI.Scheme, Host: baseUI.Host}
51-
return oauthCallbackFromOrigin(origin)
55+
return oauthCallbackURL(baseUI.Scheme, baseUI.Host)
5256
}
53-
u, err := url.Parse(strings.TrimSpace(redirectBase))
57+
ok, err := isSameSchemeAndHost(redirectBase, r)
5458
if err != nil {
55-
return "", fmt.Errorf("invalid redirect_base")
59+
return "", err
5660
}
57-
if u.Scheme != "http" && u.Scheme != "https" {
58-
return "", fmt.Errorf("invalid redirect_base: only http and https are allowed")
61+
if !ok {
62+
return "", fmt.Errorf("redirect_base does not match this UI origin")
5963
}
60-
if u.Hostname() == "" {
61-
return "", fmt.Errorf("invalid redirect_base: host is required")
62-
}
63-
if u.RawQuery != "" || u.Fragment != "" {
64-
return "", fmt.Errorf("invalid redirect_base: query and fragment are not allowed")
65-
}
66-
origin := &url.URL{Scheme: u.Scheme, Host: u.Host}
67-
if err := redirectBaseMatchesRequest(r, origin); err != nil {
64+
rs, rh := requestSchemeAndHost(r)
65+
canon := normalizeOrigin(rs, rh)
66+
scheme, host, err := splitCanonicalOriginURL(canon)
67+
if err != nil {
6868
return "", err
6969
}
70-
return oauthCallbackFromOrigin(origin)
70+
return oauthCallbackURL(scheme, host)
7171
}
7272

7373
// ResolveLogoutRedirectBase returns the UI base URL for OIDC post_logout_redirect_uri (no trailing slash).
@@ -82,6 +82,24 @@ func ResolveLogoutRedirectBase(r *http.Request, redirectBase string) (string, er
8282
return strings.TrimSuffix(callbackURI, "/callback"), nil
8383
}
8484

85+
// splitCanonicalOriginURL splits a string produced only by normalizeOrigin ("scheme://host[:port]").
86+
func splitCanonicalOriginURL(canon string) (scheme, host string, err error) {
87+
const sep = "://"
88+
i := strings.Index(canon, sep)
89+
if i < 0 {
90+
return "", "", fmt.Errorf("invalid canonical origin")
91+
}
92+
scheme = canon[:i]
93+
host = canon[i+len(sep):]
94+
if scheme != "http" && scheme != "https" {
95+
return "", "", fmt.Errorf("invalid canonical origin scheme")
96+
}
97+
if host == "" {
98+
return "", "", fmt.Errorf("invalid canonical origin host")
99+
}
100+
return scheme, host, nil
101+
}
102+
85103
func requestSchemeAndHost(r *http.Request) (scheme, host string) {
86104
scheme = "http"
87105
if r.TLS != nil {
@@ -100,6 +118,37 @@ func requestSchemeAndHost(r *http.Request) (scheme, host string) {
100118
return scheme, host
101119
}
102120

121+
// isSameSchemeAndHost parses redirectBase as an http(s) URL, validates it, and reports whether
122+
// its origin matches the effective request (see requestSchemeAndHost). Empty redirectBase
123+
// returns (false, nil). Invalid redirect_base returns (_, err).
124+
func isSameSchemeAndHost(redirectBase string, r *http.Request) (bool, error) {
125+
s := strings.TrimSpace(redirectBase)
126+
if s == "" {
127+
return false, nil
128+
}
129+
u, err := url.Parse(s)
130+
if err != nil {
131+
return false, fmt.Errorf("invalid redirect_base")
132+
}
133+
if u.Scheme != "http" && u.Scheme != "https" {
134+
return false, fmt.Errorf("invalid redirect_base: only http and https are allowed")
135+
}
136+
if u.Hostname() == "" {
137+
return false, fmt.Errorf("invalid redirect_base: host is required")
138+
}
139+
if u.RawQuery != "" || u.Fragment != "" {
140+
return false, fmt.Errorf("invalid redirect_base: query and fragment are not allowed")
141+
}
142+
if u.User != nil {
143+
return false, fmt.Errorf("invalid redirect_base: user info is not allowed")
144+
}
145+
rs, rh := requestSchemeAndHost(r)
146+
if normalizeOrigin(u.Scheme, u.Host) != normalizeOrigin(rs, rh) {
147+
return false, nil
148+
}
149+
return true, nil
150+
}
151+
103152
// cookieSecureForRequest is true when the Set-Cookie Secure attribute should be set: TLS is
104153
// configured on this proxy, or the effective request scheme is HTTPS (including when TLS
105154
// terminates at a reverse proxy and X-Forwarded-Proto is trusted).
@@ -114,16 +163,6 @@ func cookieSecureForRequest(r *http.Request) bool {
114163
return strings.EqualFold(strings.TrimSpace(scheme), "https")
115164
}
116165

117-
func redirectBaseMatchesRequest(r *http.Request, u *url.URL) error {
118-
rs, rh := requestSchemeAndHost(r)
119-
candidate := normalizeOrigin(u.Scheme, u.Host)
120-
actual := normalizeOrigin(rs, rh)
121-
if candidate != actual {
122-
return fmt.Errorf("redirect_base does not match this UI origin")
123-
}
124-
return nil
125-
}
126-
127166
func normalizeOrigin(scheme, host string) string {
128167
scheme = strings.ToLower(strings.TrimSpace(scheme))
129168
host = strings.ToLower(strings.TrimSpace(host))

0 commit comments

Comments
 (0)