diff --git a/authorize/grpc.go b/authorize/grpc.go index 0476f5903..5c0f6b79e 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -11,7 +11,6 @@ import ( "github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/config" - "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/telemetry/trace" @@ -44,7 +43,7 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe isForwardAuth := a.isForwardAuth(in) if isForwardAuth { // update the incoming http request's uri to match the forwarded URI - fwdAuthURI := getForwardAuthURL(hreq) + fwdAuthURI := urlutil.GetForwardAuthURL(hreq) in.Attributes.Request.Http.Scheme = fwdAuthURI.Scheme in.Attributes.Request.Http.Host = fwdAuthURI.Host in.Attributes.Request.Http.Path = fwdAuthURI.EscapedPath() @@ -103,26 +102,6 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe return a.handleResultDenied(ctx, in, req, res, isForwardAuthVerify, res.Allow.Reasons) } -func getForwardAuthURL(r *http.Request) *url.URL { - urqQuery := r.URL.Query().Get("uri") - u, _ := urlutil.ParseAndValidateURL(urqQuery) - if u == nil { - u = &url.URL{ - Scheme: r.Header.Get(httputil.HeaderForwardedProto), - Host: r.Header.Get(httputil.HeaderForwardedHost), - Path: r.Header.Get(httputil.HeaderForwardedURI), - } - } - originalURL := r.Header.Get(httputil.HeaderOriginalURL) - if originalURL != "" { - k, _ := urlutil.ParseAndValidateURL(originalURL) - if k != nil { - u = k - } - } - return u -} - // isForwardAuth returns if the current request is a forward auth route. func (a *Authorize) isForwardAuth(req *envoy_service_auth_v3.CheckRequest) bool { opts := a.currentOptions.Load() diff --git a/internal/urlutil/forward.go b/internal/urlutil/forward.go new file mode 100644 index 000000000..a15481ab9 --- /dev/null +++ b/internal/urlutil/forward.go @@ -0,0 +1,47 @@ +package urlutil + +import ( + "net/http" + "net/url" + "strings" +) + +// Forward headers contains information from the client-facing side of proxy +// servers that is altered or lost when a proxy is involved in the path of the +// request. +// +// https://tools.ietf.org/html/rfc7239 +// https://en.wikipedia.org/wiki/X-Forwarded-For +const ( + HeaderForwardedHost = "X-Forwarded-Host" + HeaderForwardedProto = "X-Forwarded-Proto" + HeaderForwardedURI = "X-Forwarded-Uri" // traefik + HeaderOriginalURL = "X-Original-Url" // nginx +) + +// GetForwardAuthURL gets the forward-auth URL for the given request. +func GetForwardAuthURL(r *http.Request) *url.URL { + urqQuery := r.URL.Query().Get("uri") + u, _ := ParseAndValidateURL(urqQuery) + if u == nil { + u = &url.URL{ + Scheme: r.Header.Get(HeaderForwardedProto), + Host: r.Header.Get(HeaderForwardedHost), + } + rawPath := r.Header.Get(HeaderForwardedURI) + if idx := strings.Index(rawPath, "?"); idx >= 0 { + u.RawPath = rawPath[:idx] + u.RawQuery = rawPath[idx+1:] + } else { + u.RawPath = rawPath + } + } + originalURL := r.Header.Get(HeaderOriginalURL) + if originalURL != "" { + k, _ := ParseAndValidateURL(originalURL) + if k != nil { + u = k + } + } + return u +} diff --git a/internal/urlutil/forward_test.go b/internal/urlutil/forward_test.go new file mode 100644 index 000000000..537996c5e --- /dev/null +++ b/internal/urlutil/forward_test.go @@ -0,0 +1,22 @@ +package urlutil + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetForwardAuthURL(t *testing.T) { + t.Run("double-escaping", func(t *testing.T) { + req, err := http.NewRequest("GET", "https://example.com", nil) + require.NoError(t, err) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "protected-host.tld") + req.Header.Set("X-Forwarded-Uri", "/example?a=b&c=d") + + u := GetForwardAuthURL(req) + assert.Equal(t, "https://protected-host.tld?a=b&c=d", u.String()) + }) +}