feat(configuration): disallow public suffix domains (#4855)

This adds a check to the domains configuration to ensure the domain value is not part of the public suffix list at https://publicsuffix.org. These domains are special and users cannot write cookies with this domain value, this makes them unusable with Authelia and this more readily makes that apparent.
pull/4856/head^2
James Elliott 2023-02-02 16:34:49 +11:00 committed by GitHub
parent 22d6fa18b9
commit 598ea2bb19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 49 deletions

View File

@ -137,11 +137,12 @@ cookies for this domain.
For example if Authelia is accessible via the URL `https://auth.example.com` the domain should be either For example if Authelia is accessible via the URL `https://auth.example.com` the domain should be either
`auth.example.com` or `example.com`. `auth.example.com` or `example.com`.
Please note most good DynamicDNS solutions fall into a specially protected group of domains and browsers do not allow The value must not match a domain on the [Public Suffix List](https://publicsuffix.org/list/) as browsers do not allow
you to write cookies for the root domain. i.e. if you have been assigned `john.duckdns.org` you can't use `duckdns.org` websites to write cookies for these domains. This includes most Dynamic DNS services such as `duckdns.org`. You should
for the domain value as browsers will not allow `john.duckdns.org` to read or write cookies for `duckdns.org`. use your domain instead of `duckdns.org` for this value, for example `example.duckdns.org`.
Consequently, if you have `john.duckdns.org` and `mary.duckdns.org` you cannot share cookies between these domains. Consequently, if you have `example.duckdns.org` and `example-auth.duckdns.org` you cannot share cookies between these
domains.
#### authelia_url #### authelia_url

2
go.mod
View File

@ -40,6 +40,7 @@ require (
github.com/trustelem/zxcvbn v1.0.1 github.com/trustelem/zxcvbn v1.0.1
github.com/valyala/fasthttp v1.44.0 github.com/valyala/fasthttp v1.44.0
github.com/wneessen/go-mail v0.3.8 github.com/wneessen/go-mail v0.3.8
golang.org/x/net v0.5.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
golang.org/x/term v0.4.0 golang.org/x/term v0.4.0
golang.org/x/text v0.6.0 golang.org/x/text v0.6.0
@ -109,7 +110,6 @@ require (
github.com/ysmood/leakless v0.8.0 // indirect github.com/ysmood/leakless v0.8.0 // indirect
golang.org/x/crypto v0.5.0 // indirect golang.org/x/crypto v0.5.0 // indirect
golang.org/x/mod v0.7.0 // indirect golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.4.0 // indirect golang.org/x/sys v0.4.0 // indirect
golang.org/x/tools v0.4.0 // indirect golang.org/x/tools v0.4.0 // indirect

View File

@ -267,7 +267,9 @@ const (
errFmtSessionDomainDuplicateCookieScope = "session: domain config %s: option 'domain' shares the same cookie domain scope as another configured session domain" errFmtSessionDomainDuplicateCookieScope = "session: domain config %s: option 'domain' shares the same cookie domain scope as another configured session domain"
errFmtSessionDomainPortalURLInsecure = "session: domain config %s: option 'authelia_url' does not have a secure scheme with a value of '%s'" errFmtSessionDomainPortalURLInsecure = "session: domain config %s: option 'authelia_url' does not have a secure scheme with a value of '%s'"
errFmtSessionDomainPortalURLNotInCookieScope = "session: domain config %s: option 'authelia_url' does not share a cookie scope with domain '%s' with a value of '%s'" errFmtSessionDomainPortalURLNotInCookieScope = "session: domain config %s: option 'authelia_url' does not share a cookie scope with domain '%s' with a value of '%s'"
errFmtSessionDomainInvalidDomain = "session: domain config %s: option 'domain' is not a valid domain" errFmtSessionDomainInvalidDomain = "session: domain config %s: option 'domain' is not a valid cookie domain"
errFmtSessionDomainInvalidDomainNoDots = "session: domain config %s: option 'domain' is not a valid cookie domain: must have at least a single period"
errFmtSessionDomainInvalidDomainPublic = "session: domain config %s: option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list"
) )
// Regulation Error Consts. // Regulation Error Consts.

View File

@ -104,12 +104,22 @@ func validateSessionDomainName(i int, config *schema.SessionConfiguration, valid
switch { switch {
case d.Domain == "": case d.Domain == "":
validator.Push(fmt.Errorf(errFmtSessionDomainRequired, sessionDomainDescriptor(i, d))) validator.Push(fmt.Errorf(errFmtSessionDomainRequired, sessionDomainDescriptor(i, d)))
return
case strings.HasPrefix(d.Domain, "*."): case strings.HasPrefix(d.Domain, "*."):
validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain)) validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain))
return
case strings.HasPrefix(d.Domain, "."): case strings.HasPrefix(d.Domain, "."):
validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d))) validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d)))
case !strings.Contains(d.Domain, "."):
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainNoDots, sessionDomainDescriptor(i, d)))
return
case !reDomainCharacters.MatchString(d.Domain): case !reDomainCharacters.MatchString(d.Domain):
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, sessionDomainDescriptor(i, d))) validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, sessionDomainDescriptor(i, d)))
return
}
if isCookieDomainAPublicSuffix(d.Domain) {
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainPublic, sessionDomainDescriptor(i, d)))
} }
} }

View File

@ -586,45 +586,6 @@ func TestShouldRaiseErrorOnBadRedisTLSOptionsMinVerGreaterThanMax(t *testing.T)
assert.EqualError(t, validator.Errors()[0], "session: redis: tls: option combination of 'minimum_version' and 'maximum_version' is invalid: minimum version TLS1.3 is greater than the maximum version TLS1.0") assert.EqualError(t, validator.Errors()[0], "session: redis: tls: option combination of 'minimum_version' and 'maximum_version' is invalid: minimum version TLS1.3 is greater than the maximum version TLS1.0")
} }
func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Domain = ""
config.Cookies = []schema.SessionCookieConfiguration{}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "session: option 'domain' is required")
}
func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Domain = "*.example.com"
ValidateSession(&config, validator)
assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain '*.example.com'): option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'")
}
func TestShouldRaiseErrorWhenDomainNameIsInvalid(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Domain = "example!.com"
ValidateSession(&config, validator)
assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example!.com'): option 'domain' is not a valid domain")
}
func TestShouldRaiseErrorWhenHaveDuplicatedDomainName(t *testing.T) { func TestShouldRaiseErrorWhenHaveDuplicatedDomainName(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
@ -675,10 +636,18 @@ func TestShouldRaiseErrorWhenDomainIsInvalid(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
have string have string
warnings []string
expected []string expected []string
}{ }{
{"ShouldRaiseErrorOnMissingDomain", "", []string{"session: domain config #1 (domain ''): option 'domain' is required"}}, {"ShouldNotRaiseErrorOnValidDomain", exampleDotCom, nil, nil},
{"ShouldNotRaiseErrorOnValidDomain", exampleDotCom, nil}, {"ShouldRaiseErrorOnMissingDomain", "", nil, []string{"session: domain config #1 (domain ''): option 'domain' is required"}},
{"ShouldRaiseErrorOnDomainWithInvalidChars", "example!.com", nil, []string{"session: domain config #1 (domain 'example!.com'): option 'domain' is not a valid cookie domain"}},
{"ShouldRaiseErrorOnDomainWithoutDots", "localhost", nil, []string{"session: domain config #1 (domain 'localhost'): option 'domain' is not a valid cookie domain: must have at least a single period"}},
{"ShouldRaiseErrorOnPublicDomainDuckDNS", "duckdns.org", nil, []string{"session: domain config #1 (domain 'duckdns.org'): option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list"}},
{"ShouldNotRaiseErrorOnSuffixOfPublicDomainDuckDNS", "example.duckdns.org", nil, nil},
{"ShouldRaiseWarningOnDomainWithLeadingDot", ".example.com", []string{"session: domain config #1 (domain '.example.com'): option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it"}, nil},
{"ShouldRaiseErrorOnDomainWithLeadingStarDot", "*.example.com", nil, []string{"session: domain config #1 (domain '*.example.com'): option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'"}},
{"ShouldRaiseErrorOnDomainNotSet", "", nil, []string{"session: domain config #1 (domain ''): option 'domain' is required"}},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -692,14 +661,18 @@ func TestShouldRaiseErrorWhenDomainIsInvalid(t *testing.T) {
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
Domain: tc.have, Domain: tc.have,
}, },
AutheliaURL: MustParseURL("https://auth.example.com")}, },
} }
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Warnings(), 0) require.Len(t, validator.Warnings(), len(tc.warnings))
require.Len(t, validator.Errors(), len(tc.expected)) require.Len(t, validator.Errors(), len(tc.expected))
for i, expected := range tc.warnings {
assert.EqualError(t, validator.Warnings()[i], expected)
}
for i, expected := range tc.expected { for i, expected := range tc.expected {
assert.EqualError(t, validator.Errors()[i], expected) assert.EqualError(t, validator.Errors()[i], expected)
} }

View File

@ -0,0 +1,15 @@
package validator
import (
"strings"
"golang.org/x/net/publicsuffix"
)
func isCookieDomainAPublicSuffix(domain string) (valid bool) {
var suffix string
suffix, _ = publicsuffix.PublicSuffix(domain)
return len(strings.TrimLeft(domain, ".")) == len(suffix)
}

View File

@ -0,0 +1,40 @@
package validator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsCookieDomainValid(t *testing.T) {
testCases := []struct {
domain string
expected bool
}{
{"example.com", false},
{".example.com", false},
{"*.example.com", false},
{"authelia.com", false},
{"duckdns.org", true},
{".duckdns.org", true},
{"example.duckdns.org", false},
{"192.168.2.1", false},
{"localhost", true},
{"com", true},
{"randomnada", true},
}
for _, tc := range testCases {
name := "ShouldFail"
if tc.expected {
name = "ShouldPass"
}
t.Run(tc.domain, func(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.expected, isCookieDomainAPublicSuffix(tc.domain))
})
})
}
}