mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-17 20:17:03 +02:00
* Strip the port from the request host when matching cookie domains GetCookieDomain suffix-matches the request host against each configured cookie domain. When a reverse proxy rewrites the Host header to the upstream's address, that host arrives as "host:port" and never matches, so the request falls through to the "did not match any of the specific cookie domains" warning and the shortest configured domain is used. One way to hit this is Traefik's Errors middleware: it fetches a page from a Service on port 443 with passHostHeader disabled, and forwards the Host as "<host>:443". The request reaches oauth2-proxy correctly and the resulting cookie is still usable, but every such request logs an error that suggests a misconfiguration when none exists. warnInvalidDomain, a few lines below in the same file, already calls net.SplitHostPort on the host before comparing it to the cookie domain. This makes GetCookieDomain consistent with it. Tests cover a Host header with a port, an X-Forwarded-Host header with a port, a non-standard port, and a host with a port that matches no configured domain. Signed-off-by: Kiril Jurbinsky <kiril@instaffo.com> * chore: add changelog entry for #3546 Signed-off-by: Kiril Jurbinsky <kiril@instaffo.com> * test: keep the new port entries at the end of the table The port cases were inserted between the Host and X-Forwarded-Host variants of the existing suffix-match case, which split a pair that reads as one. Move them after the last existing entry instead. Signed-off-by: Kiril Jurbinsky <kiril@instaffo.com> * test: pin that a port part can no longer match a cookie domain SplitHostPort does not require the port to be numeric, so a host such as "evil.com:.cookies.test" used to suffix-match a configured cookie domain through its port part, and the cookie was then set for that domain. Matching now runs against the host alone, so it does not. Covers both the Host header and X-Forwarded-Host, since GetRequestHost returns one or the other into the same comparison. A Host of this shape cannot be expressed through the request URL, because http.NewRequest rejects it as an invalid port, so the table gains a rawHost field that assigns req.Host after the request is built. That is how a server populates it from the wire, and pkg/upstream and pkg/middleware already set req.Host the same way in their tests. Thanks to @Lrifton92 for spotting this. Signed-off-by: Kiril Jurbinsky <kiril@instaffo.com> --------- Signed-off-by: Kiril Jurbinsky <kiril@instaffo.com>
211 lines
6.7 KiB
Go
211 lines
6.7 KiB
Go
package cookies
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Cookie Tests", func() {
|
|
Context("GetCookieDomain", func() {
|
|
type getCookieDomainTableInput struct {
|
|
host string
|
|
// rawHost sets req.Host after the request is built, for values a
|
|
// URL cannot carry, such as a host whose port part is not numeric.
|
|
rawHost string
|
|
xForwardedHost string
|
|
cookieDomains []string
|
|
expectedOutput string
|
|
}
|
|
|
|
DescribeTable("should return expected results",
|
|
func(in getCookieDomainTableInput) {
|
|
req, err := http.NewRequest(
|
|
http.MethodGet,
|
|
fmt.Sprintf("https://%s/%s", in.host, cookiePath),
|
|
nil,
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
if in.rawHost != "" {
|
|
req.Host = in.rawHost
|
|
}
|
|
|
|
if in.xForwardedHost != "" {
|
|
req.Header.Add("X-Forwarded-Host", in.xForwardedHost)
|
|
req.RemoteAddr = "127.0.0.1:4180"
|
|
trustedProxies, err := ip.ParseNetSet([]string{"127.0.0.1"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{
|
|
ReverseProxy: true,
|
|
TrustedProxies: trustedProxies,
|
|
})
|
|
}
|
|
|
|
Expect(GetCookieDomain(req, in.cookieDomains)).To(Equal(in.expectedOutput))
|
|
},
|
|
Entry("a single exact match for the Host header", getCookieDomainTableInput{
|
|
host: "www.cookies.test",
|
|
cookieDomains: []string{"www.cookies.test"},
|
|
expectedOutput: "www.cookies.test",
|
|
}),
|
|
Entry("a single exact match for the X-Forwarded-Host header", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
xForwardedHost: "www.cookies.test",
|
|
cookieDomains: []string{"www.cookies.test"},
|
|
expectedOutput: "www.cookies.test",
|
|
}),
|
|
Entry("a single suffix match for the Host header", getCookieDomainTableInput{
|
|
host: "www.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("a single suffix match for the X-Forwarded-Host header", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
xForwardedHost: "www.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("the first match is used", getCookieDomainTableInput{
|
|
host: "www.cookies.test",
|
|
cookieDomains: []string{"www.cookies.test", ".cookies.test"},
|
|
expectedOutput: "www.cookies.test",
|
|
}),
|
|
Entry("the only match is used", getCookieDomainTableInput{
|
|
host: "www.cookies.test",
|
|
cookieDomains: []string{".cookies.wrong", ".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("blank is returned for no matches", getCookieDomainTableInput{
|
|
host: "www.cookies.test",
|
|
cookieDomains: []string{".cookies.wrong", ".cookies.false"},
|
|
expectedOutput: "",
|
|
}),
|
|
Entry("a Host header carrying a port", getCookieDomainTableInput{
|
|
host: "www.cookies.test:443",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("an X-Forwarded-Host header carrying a port", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
xForwardedHost: "www.cookies.test:443",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("a non-standard port", getCookieDomainTableInput{
|
|
host: "www.cookies.test:8443",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: ".cookies.test",
|
|
}),
|
|
Entry("a host with a port that matches no domain", getCookieDomainTableInput{
|
|
host: "www.example.com:443",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: "",
|
|
}),
|
|
Entry("a Host header whose port part looks like a cookie domain", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
rawHost: "evil.com:.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: "",
|
|
}),
|
|
Entry("a Host header whose port part ends in a cookie domain", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
rawHost: "evil.com:443.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: "",
|
|
}),
|
|
Entry("an X-Forwarded-Host header whose port part looks like a cookie domain", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
xForwardedHost: "evil.com:.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: "",
|
|
}),
|
|
Entry("an X-Forwarded-Host header whose port part ends in a cookie domain", getCookieDomainTableInput{
|
|
host: "backend.cookies.internal",
|
|
xForwardedHost: "evil.com:443.cookies.test",
|
|
cookieDomains: []string{".cookies.test"},
|
|
expectedOutput: "",
|
|
}),
|
|
)
|
|
})
|
|
|
|
Context("MakeCookieFromOptions", func() {
|
|
type makeCookieFromOptionsTableInput struct {
|
|
host string
|
|
opts CookieOptions
|
|
now time.Time
|
|
expectedOutput int
|
|
}
|
|
|
|
validName := "_oauth2_proxy"
|
|
domains := []string{"www.cookies.test"}
|
|
|
|
now := time.Now()
|
|
var expectedMaxAge int
|
|
|
|
DescribeTable("should return cookies with or without expiration",
|
|
func(in makeCookieFromOptionsTableInput) {
|
|
req, err := http.NewRequest(
|
|
http.MethodGet,
|
|
fmt.Sprintf("https://%s/%s", in.host, cookiePath),
|
|
nil,
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(MakeCookieFromOptions(req, &in.opts).MaxAge).To(Equal(in.expectedOutput))
|
|
},
|
|
Entry("persistent cookie", makeCookieFromOptionsTableInput{
|
|
host: "www.cookies.test",
|
|
opts: CookieOptions{
|
|
Name: validName,
|
|
Value: "1",
|
|
Domains: domains,
|
|
Expiration: 15 * time.Minute,
|
|
SameSite: "",
|
|
Path: "",
|
|
HTTPOnly: false,
|
|
Secure: true,
|
|
},
|
|
now: now,
|
|
expectedOutput: int((15 * time.Minute).Seconds()),
|
|
}),
|
|
Entry("persistent cookie to be cleared", makeCookieFromOptionsTableInput{
|
|
host: "www.cookies.test",
|
|
opts: CookieOptions{
|
|
Name: validName,
|
|
Value: "1",
|
|
Domains: domains,
|
|
Expiration: time.Hour * -1,
|
|
SameSite: "",
|
|
Path: "",
|
|
HTTPOnly: false,
|
|
Secure: true,
|
|
},
|
|
now: now,
|
|
expectedOutput: -1,
|
|
}),
|
|
Entry("session cookie", makeCookieFromOptionsTableInput{
|
|
host: "www.cookies.test",
|
|
opts: CookieOptions{
|
|
Name: validName,
|
|
Value: "1",
|
|
Domains: domains,
|
|
Expiration: 0,
|
|
SameSite: "",
|
|
Path: "",
|
|
HTTPOnly: false,
|
|
Secure: true,
|
|
},
|
|
now: now,
|
|
expectedOutput: expectedMaxAge,
|
|
}),
|
|
)
|
|
})
|
|
})
|