Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions lib/httplib/httplib.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"net/url"
"regexp"
"strconv"
"strings"

"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Contributor Author

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?

Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Contributor Author

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

// 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") {
Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
Expand Down
68 changes: 68 additions & 0 deletions lib/httplib/httplib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Comment thread
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)
Copy link
Copy Markdown
Contributor

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.

tc.errCheck(t, err)
})
}
}
5 changes: 2 additions & 3 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4253,12 +4253,11 @@ func SSOSetWebSessionAndRedirectURL(w http.ResponseWriter, r *http.Request, resp
return trace.Wrap(err)
}

parsedURL, err := url.Parse(response.ClientRedirectURL)
parsedRedirectURL, err := httplib.OriginLocalRedirectURI(response.ClientRedirectURL)
if err != nil {
return trace.Wrap(err)
}

response.ClientRedirectURL = parsedURL.RequestURI()
response.ClientRedirectURL = parsedRedirectURL

return nil
}
Expand Down