Skip to content

Commit 9246612

Browse files
authored
Improve URL validation for external wiki and external issues (#4710)
* Improve URL validation for external wiki and external issues * Do not allow also localhost address for external URLs
1 parent 0449330 commit 9246612

File tree

5 files changed

+180
-10
lines changed

5 files changed

+180
-10
lines changed

modules/validation/binding.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package validation
66

77
import (
88
"fmt"
9-
"net/url"
109
"regexp"
1110
"strings"
1211

@@ -70,13 +69,9 @@ func addValidURLBindingRule() {
7069
},
7170
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
7271
str := fmt.Sprintf("%v", val)
73-
if len(str) != 0 {
74-
if u, err := url.ParseRequestURI(str); err != nil ||
75-
(u.Scheme != "http" && u.Scheme != "https") ||
76-
!validPort(portOnly(u.Host)) {
77-
errs.Add([]string{name}, binding.ERR_URL, "Url")
78-
return false, errs
79-
}
72+
if len(str) != 0 && !IsValidURL(str) {
73+
errs.Add([]string{name}, binding.ERR_URL, "Url")
74+
return false, errs
8075
}
8176

8277
return true, errs

modules/validation/helpers.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package validation
6+
7+
import (
8+
"net"
9+
"net/url"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/setting"
13+
)
14+
15+
var loopbackIPBlocks []*net.IPNet
16+
17+
func init() {
18+
for _, cidr := range []string{
19+
"127.0.0.0/8", // IPv4 loopback
20+
"::1/128", // IPv6 loopback
21+
} {
22+
if _, block, err := net.ParseCIDR(cidr); err == nil {
23+
loopbackIPBlocks = append(loopbackIPBlocks, block)
24+
}
25+
}
26+
}
27+
28+
func isLoopbackIP(ip string) bool {
29+
pip := net.ParseIP(ip)
30+
if pip == nil {
31+
return false
32+
}
33+
for _, block := range loopbackIPBlocks {
34+
if block.Contains(pip) {
35+
return true
36+
}
37+
}
38+
return false
39+
}
40+
41+
// IsValidURL checks if URL is valid
42+
func IsValidURL(uri string) bool {
43+
if u, err := url.ParseRequestURI(uri); err != nil ||
44+
(u.Scheme != "http" && u.Scheme != "https") ||
45+
!validPort(portOnly(u.Host)) {
46+
return false
47+
}
48+
49+
return true
50+
}
51+
52+
// IsAPIURL checks if URL is current Gitea instance API URL
53+
func IsAPIURL(uri string) bool {
54+
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))
55+
}
56+
57+
// IsValidExternalURL checks if URL is valid external URL
58+
func IsValidExternalURL(uri string) bool {
59+
if !IsValidURL(uri) || IsAPIURL(uri) {
60+
return false
61+
}
62+
63+
u, err := url.ParseRequestURI(uri)
64+
if err != nil {
65+
return false
66+
}
67+
68+
// Currently check only if not loopback IP is provided to keep compatibility
69+
if isLoopbackIP(u.Hostname()) || strings.ToLower(u.Hostname()) == "localhost" {
70+
return false
71+
}
72+
73+
// TODO: Later it should be added to allow local network IP addreses
74+
// only if allowed by special setting
75+
76+
return true
77+
}

modules/validation/helpers_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package validation
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
"code.gitea.io/gitea/modules/setting"
13+
)
14+
15+
func Test_IsValidURL(t *testing.T) {
16+
cases := []struct {
17+
description string
18+
url string
19+
valid bool
20+
}{
21+
{
22+
description: "Empty URL",
23+
url: "",
24+
valid: false,
25+
},
26+
{
27+
description: "Loobpack IPv4 URL",
28+
url: "http://127.0.1.1:5678/",
29+
valid: true,
30+
},
31+
{
32+
description: "Loobpack IPv6 URL",
33+
url: "https://[::1]/",
34+
valid: true,
35+
},
36+
{
37+
description: "Missing semicolon after schema",
38+
url: "http//meh/",
39+
valid: false,
40+
},
41+
}
42+
43+
for _, testCase := range cases {
44+
t.Run(testCase.description, func(t *testing.T) {
45+
assert.Equal(t, testCase.valid, IsValidURL(testCase.url))
46+
})
47+
}
48+
}
49+
50+
func Test_IsValidExternalURL(t *testing.T) {
51+
setting.AppURL = "https://try.gitea.io/"
52+
53+
cases := []struct {
54+
description string
55+
url string
56+
valid bool
57+
}{
58+
{
59+
description: "Current instance URL",
60+
url: "https://try.gitea.io/test",
61+
valid: true,
62+
},
63+
{
64+
description: "Loobpack IPv4 URL",
65+
url: "http://127.0.1.1:5678/",
66+
valid: false,
67+
},
68+
{
69+
description: "Current instance API URL",
70+
url: "https://try.gitea.io/api/v1/user/follow",
71+
valid: false,
72+
},
73+
{
74+
description: "Local network URL",
75+
url: "http://192.168.1.2/api/v1/user/follow",
76+
valid: true,
77+
},
78+
{
79+
description: "Local URL",
80+
url: "http://LOCALHOST:1234/whatever",
81+
valid: false,
82+
},
83+
}
84+
85+
for _, testCase := range cases {
86+
t.Run(testCase.description, func(t *testing.T) {
87+
assert.Equal(t, testCase.valid, IsValidExternalURL(testCase.url))
88+
})
89+
}
90+
}

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,7 @@ settings.external_tracker_url = External Issue Tracker URL
987987
settings.external_tracker_url_error = The external issue tracker URL is not a valid URL.
988988
settings.external_tracker_url_desc = Visitors are redirected to the external issue tracker URL when clicking on the issues tab.
989989
settings.tracker_url_format = External Issue Tracker URL Format
990+
settings.tracker_url_format_error = The external issue tracker URL format is not a valid URL.
990991
settings.tracker_issue_style = External Issue Tracker Number Format
991992
settings.tracker_issue_style.numeric = Numeric
992993
settings.tracker_issue_style.alphanumeric = Alphanumeric

routers/repo/setting.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2018 The Gitea Authors. All rights reserved.
23
// Use of this source code is governed by a MIT-style
34
// license that can be found in the LICENSE file.
45

@@ -17,6 +18,7 @@ import (
1718
"code.gitea.io/gitea/modules/log"
1819
"code.gitea.io/gitea/modules/setting"
1920
"code.gitea.io/gitea/modules/util"
21+
"code.gitea.io/gitea/modules/validation"
2022
"code.gitea.io/gitea/routers/utils"
2123
)
2224

@@ -157,7 +159,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
157159

158160
if form.EnableWiki {
159161
if form.EnableExternalWiki {
160-
if !strings.HasPrefix(form.ExternalWikiURL, "http://") && !strings.HasPrefix(form.ExternalWikiURL, "https://") {
162+
if !validation.IsValidExternalURL(form.ExternalWikiURL) {
161163
ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
162164
ctx.Redirect(repo.Link() + "/settings")
163165
return
@@ -181,11 +183,16 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
181183

182184
if form.EnableIssues {
183185
if form.EnableExternalTracker {
184-
if !strings.HasPrefix(form.ExternalTrackerURL, "http://") && !strings.HasPrefix(form.ExternalTrackerURL, "https://") {
186+
if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
185187
ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
186188
ctx.Redirect(repo.Link() + "/settings")
187189
return
188190
}
191+
if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalURL(form.TrackerURLFormat) {
192+
ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
193+
ctx.Redirect(repo.Link() + "/settings")
194+
return
195+
}
189196
units = append(units, models.RepoUnit{
190197
RepoID: repo.ID,
191198
Type: models.UnitTypeExternalTracker,

0 commit comments

Comments
 (0)