-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Prevent SSO Redirects to other origins #33853
Conversation
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
1 similar comment
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
@jentfoo this one seems worthy of a changelog entry don't you think? It's a behavior change and a security improvement, both of which are things I would think users want to know about. |
// 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 schema and provide the URI | ||
// associated with the URL. Additionally, it will ensure that the URI does not include any techniques potentially |
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 ExtractPath
is 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 word Redirect
in it (since that's the only known use case), and I tried to make sure that the Origin
concept was also in the name.
ExtractPath
sounds too generic IMO, and does not indicate the additional checks put here.
ExtractOriginLocalSafePath
I 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 httplib
is 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 httplib
because 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) SafeRedirect
also lived in httplib
@zmb3 I am not sure, I would like to hear your perspective. Here is why I was thinking we would not want a changelog:
My concern is that these improvements are very slight, and I am not sure that customers would be interested in them. However I have no objection to adding them to the changelog. I just wanted to highlight that there are a range of |
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.
I'm still trying to grok where the opaque part of the URL comes in, from the comment on the linked issue. I see how an opaque element is turned into a URL with a different hostname, but I don't see how the example exploit has an opaque part. But I'll keep thinking on it and publish these comments for now...
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
result, err := OriginLocalRedirectURI(tc.input) | ||
require.Equal(t, tc.expected, result) |
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.
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 expected: ""
fields for the error cases in the table (both since it is the zero value so does not need to be initialised but also because it is not relevant).
if you want the returned URI to be the empty string in error cases, I'd document that in the function's doc comment.
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") { |
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.
nit: the parsedURL.IsAbs()
check is redundant since the empty string for Scheme will fail the other checks.
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.
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.
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.
yeah my mistake
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.
LGTM. I now understand what's happening - the link to the opaque URL handling is a misdirection. The exploit works by having Teleport send an HTML meta redirect containing:
<meta http-equiv="refresh" content="0;URL='//example.com/'" />
and it is the browser that is redirecting to example.com. Nowhere in the Teleport source is it hitting https://github.com/golang/go/blob/c65f74d339169a5597c64a0076c17905c85b37d8/src/net/url/url.go#L1136 as described in the linked ticket.
As an aside, I could find no way to have the Chrome inspector show the HTML body of a page that does a meta refresh redirect - it's just gone. I could only verify exactly what Teleport was returning to the browser by adding some logging to the source.
However, I have reproduced the exploit locally and re-run it with the changes in this PR and verified that it does stop the exploit. So that's a LGTM from me.
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") { |
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.
yeah my mistake
This change fixes https://github.com/gravitational/security-findings/issues/69
Through the
url.Parse
and then.RequestURI()
it is expected to be a path which will redirect to the same origin. However there are potential patterns that customers could be phished to inject which would result in a redirect to another domain. This is done after the auth process, as such this would NOT result in secrets being sent to the attacker controlled domain.In this change I removed the unused
SafeRedirect
function and instead added a new functionOriginLocalRedirectURI
which will ensure that the provided URI's will remain Origin local after being interpreted by the browser.