Browse Source

Better URL validation (#1507)

* Add correct git branch name validation

* Change git refname validation error constant name

* Implement URL validation based on GoLang url.Parse method

* Backward compatibility with older Go compiler

* Add git reference name validation unit tests

* Remove unused variable in unit test

* Implement URL validation based on GoLang url.Parse method

* Backward compatibility with older Go compiler

* Add url validation unit tests
Lauris BH 3 years ago
parent
commit
f42ec6120e

+ 2 - 0
cmd/web.go

@@ -23,6 +23,7 @@ import (
23 23
 	"code.gitea.io/gitea/modules/public"
24 24
 	"code.gitea.io/gitea/modules/setting"
25 25
 	"code.gitea.io/gitea/modules/templates"
26
+	"code.gitea.io/gitea/modules/validation"
26 27
 	"code.gitea.io/gitea/routers"
27 28
 	"code.gitea.io/gitea/routers/admin"
28 29
 	apiv1 "code.gitea.io/gitea/routers/api/v1"
@@ -177,6 +178,7 @@ func runWeb(ctx *cli.Context) error {
177 178
 	reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
178 179
 
179 180
 	bindIgnErr := binding.BindIgnErr
181
+	validation.AddBindingRules()
180 182
 
181 183
 	m.Use(user.GetNotificationCount)
182 184
 

+ 1 - 1
modules/auth/admin.go

@@ -32,7 +32,7 @@ type AdminEditUserForm struct {
32 32
 	FullName                string `binding:"MaxSize(100)"`
33 33
 	Email                   string `binding:"Required;Email;MaxSize(254)"`
34 34
 	Password                string `binding:"MaxSize(255)"`
35
-	Website                 string `binding:"Url;MaxSize(255)"`
35
+	Website                 string `binding:"ValidUrl;MaxSize(255)"`
36 36
 	Location                string `binding:"MaxSize(50)"`
37 37
 	MaxRepoCreation         int
38 38
 	Active                  bool

+ 3 - 0
modules/auth/auth.go

@@ -19,6 +19,7 @@ import (
19 19
 	"code.gitea.io/gitea/modules/base"
20 20
 	"code.gitea.io/gitea/modules/log"
21 21
 	"code.gitea.io/gitea/modules/setting"
22
+	"code.gitea.io/gitea/modules/validation"
22 23
 )
23 24
 
24 25
 // IsAPIPath if URL is an api path
@@ -253,6 +254,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
253 254
 				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
254 255
 			case binding.ERR_ALPHA_DASH_DOT:
255 256
 				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
257
+			case validation.ErrGitRefName:
258
+				data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
256 259
 			case binding.ERR_SIZE:
257 260
 				data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
258 261
 			case binding.ERR_MIN_SIZE:

+ 1 - 1
modules/auth/org.go

@@ -31,7 +31,7 @@ type UpdateOrgSettingForm struct {
31 31
 	Name            string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"`
32 32
 	FullName        string `binding:"MaxSize(100)"`
33 33
 	Description     string `binding:"MaxSize(255)"`
34
-	Website         string `binding:"Url;MaxSize(255)"`
34
+	Website         string `binding:"ValidUrl;MaxSize(255)"`
35 35
 	Location        string `binding:"MaxSize(50)"`
36 36
 	MaxRepoCreation int
37 37
 }

+ 6 - 6
modules/auth/repo_form.go

@@ -87,7 +87,7 @@ func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) {
87 87
 type RepoSettingForm struct {
88 88
 	RepoName      string `binding:"Required;AlphaDashDot;MaxSize(100)"`
89 89
 	Description   string `binding:"MaxSize(255)"`
90
-	Website       string `binding:"Url;MaxSize(255)"`
90
+	Website       string `binding:"ValidUrl;MaxSize(255)"`
91 91
 	Interval      string
92 92
 	MirrorAddress string
93 93
 	Private       bool
@@ -143,7 +143,7 @@ func (f WebhookForm) ChooseEvents() bool {
143 143
 
144 144
 // NewWebhookForm form for creating web hook
145 145
 type NewWebhookForm struct {
146
-	PayloadURL  string `binding:"Required;Url"`
146
+	PayloadURL  string `binding:"Required;ValidUrl"`
147 147
 	ContentType int    `binding:"Required"`
148 148
 	Secret      string
149 149
 	WebhookForm
@@ -156,7 +156,7 @@ func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs binding.Errors) bin
156 156
 
157 157
 // NewSlackHookForm form for creating slack hook
158 158
 type NewSlackHookForm struct {
159
-	PayloadURL string `binding:"Required;Url"`
159
+	PayloadURL string `binding:"Required;ValidUrl"`
160 160
 	Channel    string `binding:"Required"`
161 161
 	Username   string
162 162
 	IconURL    string
@@ -323,7 +323,7 @@ type EditRepoFileForm struct {
323 323
 	CommitSummary string `binding:"MaxSize(100)"`
324 324
 	CommitMessage string
325 325
 	CommitChoice  string `binding:"Required;MaxSize(50)"`
326
-	NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
326
+	NewBranchName string `binding:"GitRefName;MaxSize(100)"`
327 327
 	LastCommit    string
328 328
 }
329 329
 
@@ -356,7 +356,7 @@ type UploadRepoFileForm struct {
356 356
 	CommitSummary string `binding:"MaxSize(100)"`
357 357
 	CommitMessage string
358 358
 	CommitChoice  string `binding:"Required;MaxSize(50)"`
359
-	NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
359
+	NewBranchName string `binding:"GitRefName;MaxSize(100)"`
360 360
 	Files         []string
361 361
 }
362 362
 
@@ -387,7 +387,7 @@ type DeleteRepoFileForm struct {
387 387
 	CommitSummary string `binding:"MaxSize(100)"`
388 388
 	CommitMessage string
389 389
 	CommitChoice  string `binding:"Required;MaxSize(50)"`
390
-	NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
390
+	NewBranchName string `binding:"GitRefName;MaxSize(100)"`
391 391
 }
392 392
 
393 393
 // Validate validates the fields

+ 1 - 1
modules/auth/user_form.go

@@ -103,7 +103,7 @@ type UpdateProfileForm struct {
103 103
 	FullName         string `binding:"MaxSize(100)"`
104 104
 	Email            string `binding:"Required;Email;MaxSize(254)"`
105 105
 	KeepEmailPrivate bool
106
-	Website          string `binding:"Url;MaxSize(255)"`
106
+	Website          string `binding:"ValidUrl;MaxSize(255)"`
107 107
 	Location         string `binding:"MaxSize(50)"`
108 108
 }
109 109
 

+ 102 - 0
modules/validation/binding.go

@@ -0,0 +1,102 @@
1
+// Copyright 2017 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
+	"fmt"
9
+	"net/url"
10
+	"regexp"
11
+	"strings"
12
+
13
+	"github.com/go-macaron/binding"
14
+)
15
+
16
+const (
17
+	// ErrGitRefName is git reference name error
18
+	ErrGitRefName = "GitRefNameError"
19
+)
20
+
21
+var (
22
+	// GitRefNamePattern is regular expression wirh unallowed characters in git reference name
23
+	GitRefNamePattern = regexp.MustCompile("[^\\d\\w-_\\./]")
24
+)
25
+
26
+// AddBindingRules adds additional binding rules
27
+func AddBindingRules() {
28
+	addGitRefNameBindingRule()
29
+	addValidURLBindingRule()
30
+}
31
+
32
+func addGitRefNameBindingRule() {
33
+	// Git refname validation rule
34
+	binding.AddRule(&binding.Rule{
35
+		IsMatch: func(rule string) bool {
36
+			return strings.HasPrefix(rule, "GitRefName")
37
+		},
38
+		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
39
+			str := fmt.Sprintf("%v", val)
40
+
41
+			if GitRefNamePattern.MatchString(str) {
42
+				errs.Add([]string{name}, ErrGitRefName, "GitRefName")
43
+				return false, errs
44
+			}
45
+			// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
46
+			if strings.HasPrefix(str, "/") || strings.HasSuffix(str, "/") ||
47
+				strings.HasPrefix(str, ".") || strings.HasSuffix(str, ".") ||
48
+				strings.HasSuffix(str, ".lock") ||
49
+				strings.Contains(str, "..") || strings.Contains(str, "//") {
50
+				errs.Add([]string{name}, ErrGitRefName, "GitRefName")
51
+				return false, errs
52
+			}
53
+
54
+			return true, errs
55
+		},
56
+	})
57
+}
58
+
59
+func addValidURLBindingRule() {
60
+	// URL validation rule
61
+	binding.AddRule(&binding.Rule{
62
+		IsMatch: func(rule string) bool {
63
+			return strings.HasPrefix(rule, "ValidUrl")
64
+		},
65
+		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
66
+			str := fmt.Sprintf("%v", val)
67
+			if len(str) != 0 {
68
+				if u, err := url.ParseRequestURI(str); err != nil ||
69
+					(u.Scheme != "http" && u.Scheme != "https") ||
70
+					!validPort(portOnly(u.Host)) {
71
+					errs.Add([]string{name}, binding.ERR_URL, "Url")
72
+					return false, errs
73
+				}
74
+			}
75
+
76
+			return true, errs
77
+		},
78
+	})
79
+}
80
+
81
+func portOnly(hostport string) string {
82
+	colon := strings.IndexByte(hostport, ':')
83
+	if colon == -1 {
84
+		return ""
85
+	}
86
+	if i := strings.Index(hostport, "]:"); i != -1 {
87
+		return hostport[i+len("]:"):]
88
+	}
89
+	if strings.Contains(hostport, "]") {
90
+		return ""
91
+	}
92
+	return hostport[colon+len(":"):]
93
+}
94
+
95
+func validPort(p string) bool {
96
+	for _, r := range []byte(p) {
97
+		if r < '0' || r > '9' {
98
+			return false
99
+		}
100
+	}
101
+	return true
102
+}

+ 62 - 0
modules/validation/binding_test.go

@@ -0,0 +1,62 @@
1
+// Copyright 2017 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
+	"fmt"
9
+	"net/http"
10
+	"net/http/httptest"
11
+	"testing"
12
+
13
+	"github.com/go-macaron/binding"
14
+	"github.com/stretchr/testify/assert"
15
+	"gopkg.in/macaron.v1"
16
+)
17
+
18
+const (
19
+	testRoute = "/test"
20
+)
21
+
22
+type (
23
+	validationTestCase struct {
24
+		description    string
25
+		data           interface{}
26
+		expectedErrors binding.Errors
27
+	}
28
+
29
+	handlerFunc func(interface{}, ...interface{}) macaron.Handler
30
+
31
+	modeler interface {
32
+		Model() string
33
+	}
34
+
35
+	TestForm struct {
36
+		BranchName string `form:"BranchName" binding:"GitRefName"`
37
+		URL        string `form:"ValidUrl" binding:"ValidUrl"`
38
+	}
39
+)
40
+
41
+func performValidationTest(t *testing.T, testCase validationTestCase) {
42
+	httpRecorder := httptest.NewRecorder()
43
+	m := macaron.Classic()
44
+
45
+	m.Post(testRoute, binding.Validate(testCase.data), func(actual binding.Errors) {
46
+		assert.Equal(t, fmt.Sprintf("%+v", testCase.expectedErrors), fmt.Sprintf("%+v", actual))
47
+	})
48
+
49
+	req, err := http.NewRequest("POST", testRoute, nil)
50
+	if err != nil {
51
+		panic(err)
52
+	}
53
+
54
+	m.ServeHTTP(httpRecorder, req)
55
+
56
+	switch httpRecorder.Code {
57
+	case http.StatusNotFound:
58
+		panic("Routing is messed up in test fixture (got 404): check methods and paths")
59
+	case http.StatusInternalServerError:
60
+		panic("Something bad happened on '" + testCase.description + "'")
61
+	}
62
+}

+ 142 - 0
modules/validation/refname_test.go

@@ -0,0 +1,142 @@
1
+// Copyright 2017 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/go-macaron/binding"
11
+)
12
+
13
+var gitRefNameValidationTestCases = []validationTestCase{
14
+	{
15
+		description: "Referece contains only characters",
16
+		data: TestForm{
17
+			BranchName: "test",
18
+		},
19
+		expectedErrors: binding.Errors{},
20
+	},
21
+	{
22
+		description: "Reference name contains single slash",
23
+		data: TestForm{
24
+			BranchName: "feature/test",
25
+		},
26
+		expectedErrors: binding.Errors{},
27
+	},
28
+	{
29
+		description: "Reference name contains backslash",
30
+		data: TestForm{
31
+			BranchName: "feature\\test",
32
+		},
33
+		expectedErrors: binding.Errors{
34
+			binding.Error{
35
+				FieldNames:     []string{"BranchName"},
36
+				Classification: ErrGitRefName,
37
+				Message:        "GitRefName",
38
+			},
39
+		},
40
+	},
41
+	{
42
+		description: "Reference name starts with dot",
43
+		data: TestForm{
44
+			BranchName: ".test",
45
+		},
46
+		expectedErrors: binding.Errors{
47
+			binding.Error{
48
+				FieldNames:     []string{"BranchName"},
49
+				Classification: ErrGitRefName,
50
+				Message:        "GitRefName",
51
+			},
52
+		},
53
+	},
54
+	{
55
+		description: "Reference name ends with dot",
56
+		data: TestForm{
57
+			BranchName: "test.",
58
+		},
59
+		expectedErrors: binding.Errors{
60
+			binding.Error{
61
+				FieldNames:     []string{"BranchName"},
62
+				Classification: ErrGitRefName,
63
+				Message:        "GitRefName",
64
+			},
65
+		},
66
+	},
67
+	{
68
+		description: "Reference name starts with slash",
69
+		data: TestForm{
70
+			BranchName: "/test",
71
+		},
72
+		expectedErrors: binding.Errors{
73
+			binding.Error{
74
+				FieldNames:     []string{"BranchName"},
75
+				Classification: ErrGitRefName,
76
+				Message:        "GitRefName",
77
+			},
78
+		},
79
+	},
80
+	{
81
+		description: "Reference name ends with slash",
82
+		data: TestForm{
83
+			BranchName: "test/",
84
+		},
85
+		expectedErrors: binding.Errors{
86
+			binding.Error{
87
+				FieldNames:     []string{"BranchName"},
88
+				Classification: ErrGitRefName,
89
+				Message:        "GitRefName",
90
+			},
91
+		},
92
+	},
93
+	{
94
+		description: "Reference name ends with .lock",
95
+		data: TestForm{
96
+			BranchName: "test.lock",
97
+		},
98
+		expectedErrors: binding.Errors{
99
+			binding.Error{
100
+				FieldNames:     []string{"BranchName"},
101
+				Classification: ErrGitRefName,
102
+				Message:        "GitRefName",
103
+			},
104
+		},
105
+	},
106
+	{
107
+		description: "Reference name contains multiple consecutive dots",
108
+		data: TestForm{
109
+			BranchName: "te..st",
110
+		},
111
+		expectedErrors: binding.Errors{
112
+			binding.Error{
113
+				FieldNames:     []string{"BranchName"},
114
+				Classification: ErrGitRefName,
115
+				Message:        "GitRefName",
116
+			},
117
+		},
118
+	},
119
+	{
120
+		description: "Reference name contains multiple consecutive slashes",
121
+		data: TestForm{
122
+			BranchName: "te//st",
123
+		},
124
+		expectedErrors: binding.Errors{
125
+			binding.Error{
126
+				FieldNames:     []string{"BranchName"},
127
+				Classification: ErrGitRefName,
128
+				Message:        "GitRefName",
129
+			},
130
+		},
131
+	},
132
+}
133
+
134
+func Test_GitRefNameValidation(t *testing.T) {
135
+	AddBindingRules()
136
+
137
+	for _, testCase := range gitRefNameValidationTestCases {
138
+		t.Run(testCase.description, func(t *testing.T) {
139
+			performValidationTest(t, testCase)
140
+		})
141
+	}
142
+}

+ 111 - 0
modules/validation/validurl_test.go

@@ -0,0 +1,111 @@
1
+// Copyright 2017 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/go-macaron/binding"
11
+)
12
+
13
+var urlValidationTestCases = []validationTestCase{
14
+	{
15
+		description: "Empty URL",
16
+		data: TestForm{
17
+			URL: "",
18
+		},
19
+		expectedErrors: binding.Errors{},
20
+	},
21
+	{
22
+		description: "URL without port",
23
+		data: TestForm{
24
+			URL: "http://test.lan/",
25
+		},
26
+		expectedErrors: binding.Errors{},
27
+	},
28
+	{
29
+		description: "URL with port",
30
+		data: TestForm{
31
+			URL: "http://test.lan:3000/",
32
+		},
33
+		expectedErrors: binding.Errors{},
34
+	},
35
+	{
36
+		description: "URL with IPv6 address without port",
37
+		data: TestForm{
38
+			URL: "http://[::1]/",
39
+		},
40
+		expectedErrors: binding.Errors{},
41
+	},
42
+	{
43
+		description: "URL with IPv6 address with port",
44
+		data: TestForm{
45
+			URL: "http://[::1]:3000/",
46
+		},
47
+		expectedErrors: binding.Errors{},
48
+	},
49
+	{
50
+		description: "Invalid URL",
51
+		data: TestForm{
52
+			URL: "http//test.lan/",
53
+		},
54
+		expectedErrors: binding.Errors{
55
+			binding.Error{
56
+				FieldNames:     []string{"URL"},
57
+				Classification: binding.ERR_URL,
58
+				Message:        "Url",
59
+			},
60
+		},
61
+	},
62
+	{
63
+		description: "Invalid schema",
64
+		data: TestForm{
65
+			URL: "ftp://test.lan/",
66
+		},
67
+		expectedErrors: binding.Errors{
68
+			binding.Error{
69
+				FieldNames:     []string{"URL"},
70
+				Classification: binding.ERR_URL,
71
+				Message:        "Url",
72
+			},
73
+		},
74
+	},
75
+	{
76
+		description: "Invalid port",
77
+		data: TestForm{
78
+			URL: "http://test.lan:3x4/",
79
+		},
80
+		expectedErrors: binding.Errors{
81
+			binding.Error{
82
+				FieldNames:     []string{"URL"},
83
+				Classification: binding.ERR_URL,
84
+				Message:        "Url",
85
+			},
86
+		},
87
+	},
88
+	{
89
+		description: "Invalid port with IPv6 address",
90
+		data: TestForm{
91
+			URL: "http://[::1]:3x4/",
92
+		},
93
+		expectedErrors: binding.Errors{
94
+			binding.Error{
95
+				FieldNames:     []string{"URL"},
96
+				Classification: binding.ERR_URL,
97
+				Message:        "Url",
98
+			},
99
+		},
100
+	},
101
+}
102
+
103
+func Test_ValidURLValidation(t *testing.T) {
104
+	AddBindingRules()
105
+
106
+	for _, testCase := range urlValidationTestCases {
107
+		t.Run(testCase.description, func(t *testing.T) {
108
+			performValidationTest(t, testCase)
109
+		})
110
+	}
111
+}

+ 1 - 0
options/locale/locale_en-US.ini

@@ -233,6 +233,7 @@ Content = Content
233 233
 require_error = ` cannot be empty.`
234 234
 alpha_dash_error = ` must be valid alphanumeric or dash(-_) characters.`
235 235
 alpha_dash_dot_error = ` must be valid alphanumeric, dash(-_) or dot characters.`
236
+git_ref_name_error = ` must be well formed git reference name.`
236 237
 size_error  = ` must be size %s.`
237 238
 min_size_error = ` must contain at least %s characters.`
238 239
 max_size_error = ` must contain at most %s characters.`