Skip to content

Commit 06cbf43

Browse files
committed
Reworked redirect uri validation, resolving uri references to base url, and returning the resolved url to the requesting function
1 parent d9569a5 commit 06cbf43

File tree

4 files changed

+84
-47
lines changed

4 files changed

+84
-47
lines changed

access.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,11 @@ func (s *Server) handleAuthorizationCodeRequest(w *Response, r *http.Request) *A
212212
if ret.RedirectUri == "" {
213213
ret.RedirectUri = FirstUri(ret.Client.GetRedirectUri(), s.Config.RedirectUriSeparator)
214214
}
215-
if err = ValidateUriList(ret.Client.GetRedirectUri(), ret.RedirectUri, s.Config.RedirectUriSeparator); err != nil {
215+
if realRedirectUri, err := ValidateUriList(ret.Client.GetRedirectUri(), ret.RedirectUri, s.Config.RedirectUriSeparator); err != nil {
216216
s.setErrorAndLog(w, E_INVALID_REQUEST, err, "auth_code_request=%s", "error validating client redirect")
217217
return nil
218+
} else {
219+
ret.RedirectUri = realRedirectUri
218220
}
219221
if ret.AuthorizeData.RedirectUri != ret.RedirectUri {
220222
s.setErrorAndLog(w, E_INVALID_REQUEST, errors.New("Redirect uri is different"), "auth_code_request=%s", "client redirect does not match authorization data")

authorize.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,12 @@ func (s *Server) HandleAuthorizeRequest(w *Response, r *http.Request) *Authorize
148148
ret.RedirectUri = FirstUri(ret.Client.GetRedirectUri(), s.Config.RedirectUriSeparator)
149149
}
150150

151-
if err = ValidateUriList(ret.Client.GetRedirectUri(), ret.RedirectUri, s.Config.RedirectUriSeparator); err != nil {
151+
if realRedirectUri, err := ValidateUriList(ret.Client.GetRedirectUri(), ret.RedirectUri, s.Config.RedirectUriSeparator); err != nil {
152152
w.SetErrorState(E_INVALID_REQUEST, "", ret.State)
153153
w.InternalError = err
154154
return nil
155+
} else {
156+
ret.RedirectUri = realRedirectUri
155157
}
156158

157159
w.SetRedirect(ret.RedirectUri)

urivalidate.go

+46-39
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,44 @@ func newUriValidationError(msg string, base string, redirect string) UriValidati
1818
return UriValidationError(fmt.Sprintf("%s: %s / %s", msg, base, redirect))
1919
}
2020

21+
// Parse urls, resolving uri references to base url
22+
func ParseUrls(baseUrl, redirectUrl string) (retBaseUrl, retRedirectUrl *url.URL, err error) {
23+
var base, redirect *url.URL
24+
// parse base url
25+
if base, err = url.Parse(baseUrl); err != nil {
26+
return nil, nil, err
27+
}
28+
29+
// parse redirect url
30+
if redirect, err = url.Parse(redirectUrl); err != nil {
31+
return nil, nil, err
32+
}
33+
34+
// must not have fragment
35+
if base.Fragment != "" || redirect.Fragment != "" {
36+
return nil, nil, newUriValidationError("url must not include fragment.", baseUrl, redirectUrl)
37+
}
38+
39+
// Scheme must match
40+
if redirect.Scheme != base.Scheme {
41+
return nil, nil, newUriValidationError("scheme mismatch", baseUrl, redirectUrl)
42+
}
43+
44+
// Host must match
45+
if redirect.Host != base.Host {
46+
return nil, nil, newUriValidationError("host mismatch", baseUrl, redirectUrl)
47+
}
48+
49+
// resolve references to base url
50+
retBaseUrl = (&url.URL{Scheme: base.Scheme, Host: base.Host, Path: "/"}).ResolveReference(&url.URL{Path: base.Path})
51+
retRedirectUrl = (&url.URL{Scheme: base.Scheme, Host: base.Host, Path: "/"}).ResolveReference(&url.URL{Path: redirect.Path})
52+
return
53+
}
54+
2155
// ValidateUriList validates that redirectUri is contained in baseUriList.
2256
// baseUriList may be a string separated by separator.
2357
// If separator is blank, validate only 1 URI.
24-
func ValidateUriList(baseUriList string, redirectUri string, separator string) error {
58+
func ValidateUriList(baseUriList string, redirectUri string, separator string) (realRedirectUri string, err error) {
2559
// make a list of uris
2660
var slist []string
2761
if separator != "" {
@@ -32,71 +66,44 @@ func ValidateUriList(baseUriList string, redirectUri string, separator string) e
3266
}
3367

3468
for _, sitem := range slist {
35-
err := ValidateUri(sitem, redirectUri)
69+
realRedirectUri, err = ValidateUri(sitem, redirectUri)
3670
// validated, return no error
3771
if err == nil {
38-
return nil
72+
return realRedirectUri, nil
3973
}
4074

4175
// if there was an error that is not a validation error, return it
4276
if _, iok := err.(UriValidationError); !iok {
43-
return err
77+
return "", err
4478
}
4579
}
4680

47-
return newUriValidationError("urls don't validate", baseUriList, redirectUri)
81+
return "", newUriValidationError("urls don't validate", baseUriList, redirectUri)
4882
}
4983

5084
// ValidateUri validates that redirectUri is contained in baseUri
51-
func ValidateUri(baseUri string, redirectUri string) error {
85+
func ValidateUri(baseUri string, redirectUri string) (realRedirectUri string, err error) {
5286
if baseUri == "" || redirectUri == "" {
53-
return errors.New("urls cannot be blank.")
87+
return "", errors.New("urls cannot be blank.")
5488
}
5589

56-
// parse base url
57-
base, err := url.Parse(baseUri)
58-
if err != nil {
59-
return err
60-
}
61-
62-
// parse passed url
63-
redirect, err := url.Parse(redirectUri)
90+
base, redirect, err := ParseUrls(baseUri, redirectUri)
6491
if err != nil {
65-
return err
66-
}
67-
68-
// must not have fragment
69-
if base.Fragment != "" || redirect.Fragment != "" {
70-
return errors.New("url must not include fragment.")
71-
}
72-
73-
// check if urls match
74-
if base.Scheme != redirect.Scheme {
75-
return newUriValidationError("scheme mismatch", baseUri, redirectUri)
76-
}
77-
if base.Host != redirect.Host {
78-
return newUriValidationError("host mismatch", baseUri, redirectUri)
92+
return "", err
7993
}
8094

8195
// allow exact path matches
8296
if base.Path == redirect.Path {
83-
return nil
97+
return redirect.String(), nil
8498
}
8599

86100
// ensure prefix matches are actually subpaths
87101
requiredPrefix := strings.TrimRight(base.Path, "/") + "/"
88102
if !strings.HasPrefix(redirect.Path, requiredPrefix) {
89-
return newUriValidationError("path is not a subpath", baseUri, redirectUri)
90-
}
91-
92-
// ensure prefix matches don't contain path traversals
93-
for _, s := range strings.Split(strings.TrimPrefix(redirect.Path, requiredPrefix), "/") {
94-
if s == ".." {
95-
return newUriValidationError("subpath cannot contain path traversal", baseUri, redirectUri)
96-
}
103+
return "", newUriValidationError("path prefix doesn't match", baseUri, redirectUri)
97104
}
98105

99-
return nil
106+
return redirect.String(),nil
100107
}
101108

102109
// Returns the first uri from an uri list

urivalidate_test.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,62 @@ func TestURIValidate(t *testing.T) {
1010
// Exact match
1111
"http://localhost:14000/appauth",
1212
"http://localhost:14000/appauth",
13+
"http://localhost:14000/appauth",
1314
},
1415
{
1516
// Trailing slash
1617
"http://www.google.com/myapp",
1718
"http://www.google.com/myapp/",
19+
"http://www.google.com/myapp/",
1820
},
1921
{
2022
// Exact match with trailing slash
2123
"http://www.google.com/myapp/",
2224
"http://www.google.com/myapp/",
25+
"http://www.google.com/myapp/",
2326
},
2427
{
2528
// Subpath
2629
"http://www.google.com/myapp",
2730
"http://www.google.com/myapp/interface/implementation",
31+
"http://www.google.com/myapp/interface/implementation",
2832
},
2933
{
3034
// Subpath with trailing slash
3135
"http://www.google.com/myapp/",
3236
"http://www.google.com/myapp/interface/implementation",
37+
"http://www.google.com/myapp/interface/implementation",
3338
},
3439
{
3540
// Subpath with things that are close to path traversals, but aren't
3641
"http://www.google.com/myapp",
3742
"http://www.google.com/myapp/.../..implementation../...",
43+
"http://www.google.com/myapp/.../..implementation../...",
3844
},
3945
{
4046
// If the allowed basepath contains path traversals, allow them?
4147
"http://www.google.com/traversal/../allowed",
4248
"http://www.google.com/traversal/../allowed/with/subpath",
49+
"http://www.google.com/allowed/with/subpath",
50+
},
51+
{
52+
// Backslashes
53+
"https://mysafewebsite.com/secure/redirect",
54+
"https://mysafewebsite.com/secure/redirect/\\../\\../\\../evil",
55+
"https://mysafewebsite.com/secure/redirect/%5C../%5C../%5C../evil",
56+
},
57+
{
58+
// Backslashes
59+
"https://mysafewebsite.com/secure/redirect",
60+
"https://mysafewebsite.com/secure/redirect/\\..\\../\\../evil",
61+
"https://mysafewebsite.com/secure/redirect/%5C..%5C../%5C../evil",
4362
},
4463
}
4564
for _, v := range valid {
46-
if err := ValidateUri(v[0], v[1]); err != nil {
65+
if realRedirectUri, err := ValidateUri(v[0], v[1]); err != nil {
4766
t.Errorf("Expected ValidateUri(%s, %s) to succeed, got %v", v[0], v[1], err)
67+
} else if len(v) == 3 && realRedirectUri != v[2] {
68+
t.Errorf("Expected ValidateUri(%s, %s) to return uri %s, got %s", v[0], v[1], v[2], realRedirectUri)
4869
}
4970
}
5071

@@ -89,32 +110,37 @@ func TestURIValidate(t *testing.T) {
89110
"http://www.google.com/myapp",
90111
"http://www.google.com/myapp../test",
91112
},
113+
{
114+
// Backslashes
115+
"https://mysafewebsite.com/secure/redirect",
116+
"https://mysafewebsite.com/secure%2fredirect/../evil",
117+
},
92118
}
93119
for _, v := range invalid {
94-
if err := ValidateUri(v[0], v[1]); err == nil {
120+
if _, err := ValidateUri(v[0], v[1]); err == nil {
95121
t.Errorf("Expected ValidateUri(%s, %s) to fail", v[0], v[1])
96122
}
97123
}
98124
}
99125

100126
func TestURIListValidate(t *testing.T) {
101127
// V1
102-
if err := ValidateUriList("http://localhost:14000/appauth", "http://localhost:14000/appauth", ""); err != nil {
128+
if _, err := ValidateUriList("http://localhost:14000/appauth", "http://localhost:14000/appauth", ""); err != nil {
103129
t.Errorf("V1: %s", err)
104130
}
105131

106132
// V2
107-
if err := ValidateUriList("http://localhost:14000/appauth", "http://localhost:14000/app", ""); err == nil {
133+
if _, err := ValidateUriList("http://localhost:14000/appauth", "http://localhost:14000/app", ""); err == nil {
108134
t.Error("V2 should have failed")
109135
}
110136

111137
// V3
112-
if err := ValidateUriList("http://xxx:14000/appauth;http://localhost:14000/appauth", "http://localhost:14000/appauth", ";"); err != nil {
138+
if _, err := ValidateUriList("http://xxx:14000/appauth;http://localhost:14000/appauth", "http://localhost:14000/appauth", ";"); err != nil {
113139
t.Errorf("V3: %s", err)
114140
}
115141

116142
// V4
117-
if err := ValidateUriList("http://xxx:14000/appauth;http://localhost:14000/appauth", "http://localhost:14000/app", ";"); err == nil {
143+
if _, err := ValidateUriList("http://xxx:14000/appauth;http://localhost:14000/appauth", "http://localhost:14000/app", ";"); err == nil {
118144
t.Error("V4 should have failed")
119145
}
120146
}

0 commit comments

Comments
 (0)