mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-01 12:11:19 +02:00
* update to release version v7.15.5 * Merge commit from fork Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: blakeroberts-wk <blake.roberts@workiva.com> * Merge commit from fork Signed-off-by: Jan Larwig <jan@larwig.com> * Merge commit from fork * fix: validate trusted IP proxy headers Respect trusted proxy boundaries before using real-client-IP headers for authentication bypass decisions and safely traverse X-Forwarded-For chains. Signed-off-by: Jan Larwig <jan@larwig.com> * fix: trusted-ip header bypass Signed-off-by: Jan Larwig <jan@larwig.com> * docs: add changelog entry Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> * docs: changelog for v7.15.5 Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> * docs: update order of owners for prow Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> * ci: make the linter happy again Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jan Larwig <jan@larwig.com> Co-authored-by: blakeroberts-wk <blake.roberts@workiva.com> Co-authored-by: Jan Larwig <jan.larwig@digits.schwarz>
322 lines
11 KiB
Go
322 lines
11 KiB
Go
package util_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Util Suite", func() {
|
|
const (
|
|
proto = "http"
|
|
host = "www.oauth2proxy.test"
|
|
uriWithQueryParams = "/test/endpoint?query=param"
|
|
uriNoQueryParams = "/test/endpoint"
|
|
)
|
|
var req *http.Request
|
|
var trustedProxies *ip.NetSet
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
trustedProxies, err = ip.ParseNetSet([]string{"127.0.0.1"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
req = httptest.NewRequest(
|
|
http.MethodGet,
|
|
fmt.Sprintf("%s://%s%s", proto, host, uriWithQueryParams),
|
|
nil,
|
|
)
|
|
})
|
|
|
|
Context("GetRequestHost", func() {
|
|
Context("trusted forwarded headers are disabled", func() {
|
|
BeforeEach(func() {
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
|
})
|
|
|
|
It("returns the host", func() {
|
|
Expect(util.GetRequestHost(req)).To(Equal(host))
|
|
})
|
|
|
|
It("ignores X-Forwarded-Host and returns the host", func() {
|
|
req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
|
|
Expect(util.GetRequestHost(req)).To(Equal(host))
|
|
})
|
|
})
|
|
|
|
Context("trusted forwarded headers are enabled", func() {
|
|
BeforeEach(func() {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
})
|
|
|
|
It("returns the host if X-Forwarded-Host is not present", func() {
|
|
Expect(util.GetRequestHost(req)).To(Equal(host))
|
|
})
|
|
|
|
It("returns the X-Forwarded-Host when present", func() {
|
|
req.Header.Add("X-Forwarded-Host", "external.oauth2proxy.text")
|
|
Expect(util.GetRequestHost(req)).To(Equal("external.oauth2proxy.text"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("GetRequestProto", func() {
|
|
Context("trusted forwarded headers are disabled", func() {
|
|
BeforeEach(func() {
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
|
})
|
|
|
|
It("returns the scheme", func() {
|
|
Expect(util.GetRequestProto(req)).To(Equal(proto))
|
|
})
|
|
|
|
It("ignores X-Forwarded-Proto and returns the scheme", func() {
|
|
req.Header.Add("X-Forwarded-Proto", "https")
|
|
Expect(util.GetRequestProto(req)).To(Equal(proto))
|
|
})
|
|
})
|
|
|
|
Context("trusted forwarded headers are enabled", func() {
|
|
BeforeEach(func() {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
})
|
|
|
|
It("returns the scheme if X-Forwarded-Proto is not present", func() {
|
|
Expect(util.GetRequestProto(req)).To(Equal(proto))
|
|
})
|
|
|
|
It("returns the X-Forwarded-Proto when present", func() {
|
|
req.Header.Add("X-Forwarded-Proto", "https")
|
|
Expect(util.GetRequestProto(req)).To(Equal("https"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("GetRequestURI", func() {
|
|
Context("trusted forwarded headers are disabled", func() {
|
|
BeforeEach(func() {
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
|
})
|
|
|
|
It("returns the URI (with query params)", func() {
|
|
Expect(util.GetRequestURI(req)).To(Equal(uriWithQueryParams))
|
|
})
|
|
|
|
It("ignores X-Forwarded-Uri and returns the URI (with query params)", func() {
|
|
req.Header.Add("X-Forwarded-Uri", "/some/other/path")
|
|
Expect(util.GetRequestURI(req)).To(Equal(uriWithQueryParams))
|
|
})
|
|
})
|
|
|
|
Context("trusted forwarded headers are enabled", func() {
|
|
BeforeEach(func() {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
})
|
|
|
|
It("returns the URI if X-Forwarded-Uri is not present (with query params)", func() {
|
|
Expect(util.GetRequestURI(req)).To(Equal(uriWithQueryParams))
|
|
})
|
|
|
|
It("returns the X-Forwarded-Uri when present (with query params)", func() {
|
|
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
|
|
Expect(util.GetRequestURI(req)).To(Equal("/some/other/path?query=param"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("GetRequestPath", func() {
|
|
DescribeTable("preserves unambiguous paths without changing the request",
|
|
func(target, expected string) {
|
|
for _, forwarded := range []bool{false, true} {
|
|
req = httptest.NewRequest(http.MethodGet, target, nil)
|
|
if forwarded {
|
|
req = httptest.NewRequest(http.MethodGet, "/oauth2/auth", nil)
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true, TrustedProxies: trustedProxies,
|
|
})
|
|
req.Header.Set("X-Forwarded-Uri", target)
|
|
}
|
|
originalURL, originalTarget := *req.URL, req.RequestURI
|
|
requestPath, err := util.GetRequestPath(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(requestPath).To(Equal(expected))
|
|
Expect(*req.URL).To(Equal(originalURL))
|
|
Expect(req.RequestURI).To(Equal(originalTarget))
|
|
}
|
|
},
|
|
Entry("root", "/", "/"),
|
|
Entry("public path", "/public/file", "/public/file"),
|
|
Entry("protected path", "/protected/file", "/protected/file"),
|
|
Entry("trailing slash", "/public/", "/public/"),
|
|
Entry("query ignored", "/public/file?next=/../protected;v=1&tag=%23", "/public/file"),
|
|
Entry("invalid query escape does not affect the path", "/public/file?q=%zz", "/public/file"),
|
|
Entry("escaped space", "/public/a%20b", "/public/a b"),
|
|
Entry("escaped letter", "/public/%66ile", "/public/file"),
|
|
Entry("escaped Unicode", "/public/caf%C3%A9", "/public/caf\u00e9"),
|
|
Entry("escaped slash", "/public%2ffile", "/public/file"),
|
|
Entry("literal plus", "/public/a+b", "/public/a+b"),
|
|
Entry("escaped plus", "/public/a%2bb", "/public/a+b"),
|
|
Entry("escaped percent", "/public/100%25", "/public/100%"),
|
|
Entry("dot within a segment", "/public/file.txt", "/public/file.txt"),
|
|
Entry("multiple dots are not a parent segment", "/public/...", "/public/..."),
|
|
)
|
|
|
|
DescribeTable("declines ambiguous or invalid original request targets",
|
|
func(target string) {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true, TrustedProxies: trustedProxies,
|
|
})
|
|
req.Header.Set("X-Forwarded-Uri", target)
|
|
requestPath, err := util.GetRequestPath(req)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(requestPath).To(BeEmpty())
|
|
Expect(err.Error()).NotTo(ContainSubstring("sensitive-value"))
|
|
},
|
|
Entry("literal parent segment", "/public/../protected?token=sensitive-value"),
|
|
Entry("encoded parent segment", "/public/%2e%2E/protected"),
|
|
Entry("encoded slash exposes parent segment", "/public%2f..%2fprotected"),
|
|
Entry("current segment", "/public/./file"),
|
|
Entry("terminal parent segment", "/public/.."),
|
|
Entry("terminal current segment", "/public/."),
|
|
Entry("leading double slash", "//protected/public"),
|
|
Entry("repeated slash", "/public//file"),
|
|
Entry("encoded repeated slash", "/public/%2ffile"),
|
|
Entry("matrix parent segment", "/public/..;/protected"),
|
|
Entry("encoded matrix parent segment", "/public/%2e%2e%3b/protected"),
|
|
Entry("valid matrix parameter", "/public/file;version=1"),
|
|
Entry("matrix parameter containing a slash", "/protected;/public"),
|
|
Entry("invalid escape", "/public/%zz?token=sensitive-value"),
|
|
Entry("incomplete escape", "/public/%"),
|
|
Entry("relative target", "public/file"),
|
|
Entry("absolute target", "https://example.com/public/file"),
|
|
Entry("authority target", "example.com:443"),
|
|
Entry("asterisk target", "*"),
|
|
Entry("literal fragment", "/public#sensitive-value"),
|
|
Entry("encoded fragment", "/public%23sensitive-value"),
|
|
Entry("encoded question mark", "/public%3fsensitive-value"),
|
|
Entry("backslash", "/public\\..\\protected"),
|
|
Entry("encoded backslash", "/public%5c..%5cprotected"),
|
|
Entry("encoded control character", "/public/%00"),
|
|
Entry("encoded delete character", "/public/%7f"),
|
|
Entry("unescaped space", "/public/a b"),
|
|
)
|
|
|
|
It("ignores a forged forwarded URI from an untrusted peer", func() {
|
|
req.RemoteAddr = "192.0.2.10:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true, TrustedProxies: trustedProxies,
|
|
})
|
|
req.Header.Set("X-Forwarded-Uri", "/public")
|
|
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
|
})
|
|
|
|
Context("trusted forwarded headers are disabled", func() {
|
|
BeforeEach(func() {
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
|
})
|
|
|
|
It("returns the URI (without query params)", func() {
|
|
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
|
})
|
|
|
|
It("declines fragment content in a parsed request path", func() {
|
|
// Simulate net/http ParseRequestURI preserving '#' in URL.Path.
|
|
req.URL.Path = "/foo/secret#/bar"
|
|
req.URL.RawPath = "/foo/secret%23/bar"
|
|
requestPath, err := util.GetRequestPath(req)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(requestPath).To(BeEmpty())
|
|
})
|
|
|
|
It("declines encoded number signs", func() {
|
|
req = httptest.NewRequest(
|
|
http.MethodGet,
|
|
fmt.Sprintf("%s://%s/foo/secret%%23/bar?query=param", proto, host),
|
|
nil,
|
|
)
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{})
|
|
requestPath, err := util.GetRequestPath(req)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(requestPath).To(BeEmpty())
|
|
})
|
|
|
|
It("ignores X-Forwarded-Uri and returns the URI (without query params)", func() {
|
|
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
|
|
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
|
})
|
|
})
|
|
|
|
Context("trusted forwarded headers are enabled", func() {
|
|
BeforeEach(func() {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
})
|
|
|
|
It("returns the URI if X-Forwarded-Uri is not present (without query params)", func() {
|
|
Expect(util.GetRequestPath(req)).To(Equal(uriNoQueryParams))
|
|
})
|
|
|
|
It("returns the X-Forwarded-Uri when present (without query params)", func() {
|
|
req.Header.Add("X-Forwarded-Uri", "/some/other/path?query=param")
|
|
Expect(util.GetRequestPath(req)).To(Equal("/some/other/path"))
|
|
})
|
|
|
|
It("declines fragment-like suffixes from the X-Forwarded-Uri", func() {
|
|
req.Header.Add("X-Forwarded-Uri", "/foo/secret%23/bar?query=param")
|
|
requestPath, err := util.GetRequestPath(req)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(requestPath).To(BeEmpty())
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("CanTrustForwardedHeaders", func() {
|
|
It("returns false when no scope is present", func() {
|
|
Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
|
|
})
|
|
|
|
It("returns true when the remote address is trusted", func() {
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
|
|
Expect(util.CanTrustForwardedHeaders(req)).To(BeTrue())
|
|
})
|
|
|
|
It("returns false when the remote address is untrusted", func() {
|
|
req.RemoteAddr = "192.0.2.10:4180"
|
|
req = middleware.AddRequestScope(req, &middleware.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
|
|
Expect(util.CanTrustForwardedHeaders(req)).To(BeFalse())
|
|
})
|
|
})
|
|
})
|