-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Prevent SSO Redirects to other origins #33853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import ( | |
| "net/url" | ||
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/gravitational/roundtrip" | ||
| "github.com/gravitational/trace" | ||
|
|
@@ -249,14 +250,24 @@ func RewritePaths(next http.Handler, rewrites ...RewritePair) http.Handler { | |
| }) | ||
| } | ||
|
|
||
| // SafeRedirect performs a relative redirect to the URI part of the provided redirect URL | ||
| func SafeRedirect(w http.ResponseWriter, r *http.Request, redirectURL string) error { | ||
| // OriginLocalRedirectURI will take an incoming URL including optionally the host and scheme and return the URI | ||
| // associated with the URL. Additionally, it will ensure that the URI does not include any techniques potentially | ||
| // used to redirect to a different origin. | ||
| func OriginLocalRedirectURI(redirectURL string) (string, error) { | ||
| parsedURL, err := url.Parse(redirectURL) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| return "", trace.Wrap(err) | ||
| } else if parsedURL.IsAbs() && (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually not redundant. It's possible for a non-absolute url to be provided, one which has no scheme. Removing this will cause some of the existing test cases to fail.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah my mistake |
||
| return "", trace.BadParameter("Invalid scheme: %s", parsedURL.Scheme) | ||
| } | ||
| http.Redirect(w, r, parsedURL.RequestURI(), http.StatusFound) | ||
| return nil | ||
|
|
||
| resultURI := parsedURL.RequestURI() | ||
| if strings.HasPrefix(resultURI, "//") { | ||
| return "", trace.BadParameter("Invalid double slash redirect") | ||
| } else if strings.Contains(resultURI, "@") { | ||
| return "", trace.BadParameter("Basic Auth not allowed in redirect") | ||
| } | ||
| return resultURI, nil | ||
| } | ||
|
|
||
| // ResponseStatusRecorder is an http.ResponseWriter that records the response status code. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,3 +407,71 @@ func TestSetRedirectPageContentSecurityPolicy(t *testing.T) { | |
| require.Contains(t, actualCsp, expectedCspSubString) | ||
| } | ||
| } | ||
|
|
||
| func TestOriginLocalRedirectURI(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| testCases := []struct { | ||
| name string | ||
| input string | ||
| expected string | ||
| errCheck require.ErrorAssertionFunc | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| input: "", | ||
| expected: "/", | ||
| errCheck: require.NoError, | ||
| }, | ||
| { | ||
| name: "simple path", | ||
| input: "/foo", | ||
| expected: "/foo", | ||
| errCheck: require.NoError, | ||
| }, | ||
| { | ||
| name: "host only", | ||
| input: "https://localhost", | ||
| expected: "/", | ||
| errCheck: require.NoError, | ||
| }, | ||
| { | ||
| name: "host and simple path", | ||
| input: "https://localhost/bar", | ||
| expected: "/bar", | ||
| errCheck: require.NoError, | ||
| }, | ||
| { | ||
| name: "double slash redirect with host", | ||
| input: "https://localhost//goteleport.com/", | ||
| expected: "", | ||
| errCheck: require.Error, | ||
| }, | ||
| { | ||
| name: "basic auth redirect with host", | ||
| input: "https://localhost/@goteleport.com/", | ||
| expected: "", | ||
| errCheck: require.Error, | ||
| }, | ||
| { | ||
| name: "ftp scheme", | ||
| input: "ftp://localhost", | ||
| expected: "", | ||
| errCheck: require.Error, | ||
| }, | ||
| { | ||
| name: "invalid url", | ||
| input: "https://foo com", | ||
| expected: "", | ||
| errCheck: require.Error, | ||
| }, | ||
|
jentfoo marked this conversation as resolved.
|
||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| result, err := OriginLocalRedirectURI(tc.input) | ||
| require.Equal(t, tc.expected, result) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is not usual for the non-error parameter to be relevant when there is an error result. I'd suggest moving this into the non-error if block, and remove the if you want the returned URI to be the empty string in error cases, I'd document that in the function's doc comment. |
||
| tc.errCheck(t, err) | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On first read I was very confused by what "the URI associated with the URL" meant.
Would it make more sense to call this function something like
ExtractPath?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unclear to me if
ExtractPathis a better name. This specifically is trying to extract the path in a way that is safe for redirects when the origin must remain consistent. So I tried to have the wordRedirectin it (since that's the only known use case), and I tried to make sure that theOriginconcept was also in the name.ExtractPathsounds too generic IMO, and does not indicate the additional checks put here.ExtractOriginLocalSafePathI think could be a good option, but I don't feel strongly about it. Do you like that naming at all?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah I see that it's doing quite a bit more than that.
I wonder if
httplibis the right place for this - it seems to be a pretty specialized function written specifically to handle SSO redirects. Should it be located closer to the code that handles SSO?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong preference on my end. I put it in
httplibbecause theoretically this is useful for any origin local redirects (though SSO was the only place I could find where we currently need this). I suspect this fairly generic quality is why the prior (unused)SafeRedirectalso lived inhttplib