Skip to content

Commit f7d7f1c

Browse files
committed
feat: add psl checks to the oauth controller is safe redirect check
1 parent e7d26f4 commit f7d7f1c

2 files changed

Lines changed: 106 additions & 10 deletions

File tree

internal/controller/oauth_controller.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/tinyauthapp/tinyauth/internal/service"
1313
"github.com/tinyauthapp/tinyauth/internal/utils"
1414
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
15+
"github.com/weppos/publicsuffix-go/publicsuffix"
1516
"go.uber.org/dig"
1617

1718
"github.com/gin-gonic/gin"
@@ -329,16 +330,33 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
329330
}
330331

331332
// exact match
332-
if u.Host == tu.Host {
333+
if strings.EqualFold(u.Host, tu.Host) {
333334
return true
334335
}
335336

336-
// subdomain match (trim the tinyauth part)
337+
// if subdomains are disabled, end here
338+
if !controller.config.Auth.SubdomainsEnabled {
339+
continue
340+
}
341+
342+
// get the root domain (e.g. tinyauth.example.com -> example.com or
343+
// tinyauth.sub.example.com -> sub.example.com)
337344
_, root, ok := strings.Cut(tu.Host, ".")
338345
if !ok {
339346
continue
340347
}
341-
if strings.HasSuffix(u.Host, "."+root) {
348+
349+
root = strings.ToLower(root)
350+
351+
// check if the root domain is in the psl
352+
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, root, nil)
353+
354+
if err != nil {
355+
continue
356+
}
357+
358+
// subdomain match
359+
if strings.HasSuffix(strings.ToLower(u.Host), "."+root) {
342360
return true
343361
}
344362
}

internal/controller/oauth_controller_test.go

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,35 @@ func TestOAuthController(t *testing.T) {
1616
cfg, runtime := test.CreateTestConfigs(t)
1717

1818
type testCase struct {
19-
description string
20-
run func(ctrl *OAuthController)
19+
description string
20+
run func(ctrl *OAuthController)
21+
trustedDomains []string
22+
subdomainsEnabled bool
2123
}
2224

2325
tests := []testCase{
2426
{
25-
description: "Test exact match of redirect URI",
27+
description: "Test exact match of redirect URI",
28+
trustedDomains: []string{"https://tinyauth.example.com"},
29+
subdomainsEnabled: true,
2630
run: func(ctrl *OAuthController) {
2731
redirectUri := "https://tinyauth.example.com"
2832
assert.True(t, ctrl.isRedirectSafe(redirectUri))
2933
},
3034
},
3135
{
32-
description: "Test subdomain match of redirect URI",
36+
description: "Test subdomain match of redirect URI",
37+
trustedDomains: []string{"https://tinyauth.example.com"},
38+
subdomainsEnabled: true,
3339
run: func(ctrl *OAuthController) {
3440
redirectUri := "https://sub.example.com"
3541
assert.True(t, ctrl.isRedirectSafe(redirectUri))
3642
},
3743
},
3844
{
39-
description: "Test different trusted domain",
45+
description: "Test different trusted domain",
46+
trustedDomains: []string{"https://tinyauth.example.com", "https://tinyauth.foo.com"},
47+
subdomainsEnabled: true,
4048
run: func(ctrl *OAuthController) {
4149
redirectUri := "https://app.foo.com"
4250
assert.True(t, ctrl.isRedirectSafe(redirectUri))
@@ -45,7 +53,7 @@ func TestOAuthController(t *testing.T) {
4553
{
4654
description: "Test invalid redirect URI",
4755
run: func(ctrl *OAuthController) {
48-
redirectUri := "https://malicious.com"
56+
redirectUri := "https:/malicious"
4957
assert.False(t, ctrl.isRedirectSafe(redirectUri))
5058
},
5159
},
@@ -57,12 +65,79 @@ func TestOAuthController(t *testing.T) {
5765
},
5866
},
5967
{
60-
description: "Test redirect URI with different scheme",
68+
description: "Test redirect URI with different scheme",
69+
trustedDomains: []string{"https://tinyauth.example.com"},
70+
subdomainsEnabled: true,
6171
run: func(ctrl *OAuthController) {
6272
redirectUri := "http://tinyauth.example.com"
6373
assert.False(t, ctrl.isRedirectSafe(redirectUri))
6474
},
6575
},
76+
{
77+
description: "Test redirect URI with different port",
78+
trustedDomains: []string{"https://tinyauth.example.com"},
79+
subdomainsEnabled: true,
80+
run: func(ctrl *OAuthController) {
81+
redirectUri := "https://tinyauth.example.com:8080"
82+
assert.False(t, ctrl.isRedirectSafe(redirectUri))
83+
},
84+
},
85+
{
86+
// weird case, subdomains enabled and domain without subdomain can't happen
87+
description: "Test with trusted domain that's in PSL when split",
88+
trustedDomains: []string{"https://example.com"}, // will become .com which we
89+
// obviously don't want to allow
90+
subdomainsEnabled: true,
91+
run: func(ctrl *OAuthController) {
92+
redirectUri := "https://sub.example.com"
93+
assert.False(t, ctrl.isRedirectSafe(redirectUri))
94+
},
95+
},
96+
{
97+
description: "Test subdomain redirect URI when subdomains are disabled",
98+
trustedDomains: []string{"https://tinyauth.example.com"},
99+
subdomainsEnabled: false,
100+
run: func(ctrl *OAuthController) {
101+
redirectUri := "https://sub.tinyauth.example.com"
102+
assert.False(t, ctrl.isRedirectSafe(redirectUri))
103+
},
104+
},
105+
{
106+
description: "Test domain like the .co.uk",
107+
trustedDomains: []string{"https://example.co.uk"},
108+
subdomainsEnabled: true,
109+
run: func(ctrl *OAuthController) {
110+
redirectUri := "https://sub.example.co.uk"
111+
assert.False(t, ctrl.isRedirectSafe(redirectUri))
112+
},
113+
},
114+
{
115+
description: "Test domain like the .co.uk with subdomains disabled",
116+
trustedDomains: []string{"https://example.co.uk"},
117+
subdomainsEnabled: false,
118+
run: func(ctrl *OAuthController) {
119+
redirectUri := "https://example.co.uk"
120+
assert.True(t, ctrl.isRedirectSafe(redirectUri))
121+
},
122+
},
123+
{
124+
description: "Test caps domain",
125+
trustedDomains: []string{"https://TINYAUTH.ExAmpLe.com"},
126+
subdomainsEnabled: true,
127+
run: func(ctrl *OAuthController) {
128+
redirectUri := "https://sUb.ExAmPle.com"
129+
assert.True(t, ctrl.isRedirectSafe(redirectUri))
130+
},
131+
},
132+
{
133+
description: "Test edge case with @",
134+
trustedDomains: []string{"https://tinyauth.example.com"},
135+
subdomainsEnabled: true,
136+
run: func(ctrl *OAuthController) {
137+
redirectUri := "https://malicious.example.com@evil.com"
138+
assert.False(t, ctrl.isRedirectSafe(redirectUri))
139+
},
140+
},
66141
}
67142

68143
// TODO: add auth service
@@ -71,6 +146,9 @@ func TestOAuthController(t *testing.T) {
71146
router := gin.Default()
72147
group := router.Group("/api")
73148
gin.SetMode(gin.TestMode)
149+
// overwrite the trusted domains and subdomain setting for each test case
150+
runtime.TrustedDomains = tc.trustedDomains
151+
cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled
74152
ctrl := NewOAuthController(OAuthControllerInput{
75153
Log: log,
76154
Config: &cfg,

0 commit comments

Comments
 (0)