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
parent
22d6fa18b9
commit
598ea2bb19
|
@ -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
2
go.mod
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue