Skip to content

Commit f42ec61

Browse files
lafriksappleboy
authored andcommitted
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
1 parent 941281a commit f42ec61

File tree

11 files changed

+432
-9
lines changed

11 files changed

+432
-9
lines changed

cmd/web.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"code.gitea.io/gitea/modules/public"
2424
"code.gitea.io/gitea/modules/setting"
2525
"code.gitea.io/gitea/modules/templates"
26+
"code.gitea.io/gitea/modules/validation"
2627
"code.gitea.io/gitea/routers"
2728
"code.gitea.io/gitea/routers/admin"
2829
apiv1 "code.gitea.io/gitea/routers/api/v1"
@@ -177,6 +178,7 @@ func runWeb(ctx *cli.Context) error {
177178
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
178179

179180
bindIgnErr := binding.BindIgnErr
181+
validation.AddBindingRules()
180182

181183
m.Use(user.GetNotificationCount)
182184

modules/auth/admin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type AdminEditUserForm struct {
3232
FullName string `binding:"MaxSize(100)"`
3333
Email string `binding:"Required;Email;MaxSize(254)"`
3434
Password string `binding:"MaxSize(255)"`
35-
Website string `binding:"Url;MaxSize(255)"`
35+
Website string `binding:"ValidUrl;MaxSize(255)"`
3636
Location string `binding:"MaxSize(50)"`
3737
MaxRepoCreation int
3838
Active bool

modules/auth/auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"code.gitea.io/gitea/modules/base"
2020
"code.gitea.io/gitea/modules/log"
2121
"code.gitea.io/gitea/modules/setting"
22+
"code.gitea.io/gitea/modules/validation"
2223
)
2324

2425
// IsAPIPath if URL is an api path
@@ -253,6 +254,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
253254
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
254255
case binding.ERR_ALPHA_DASH_DOT:
255256
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")
256259
case binding.ERR_SIZE:
257260
data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
258261
case binding.ERR_MIN_SIZE:

modules/auth/org.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type UpdateOrgSettingForm struct {
3131
Name string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"`
3232
FullName string `binding:"MaxSize(100)"`
3333
Description string `binding:"MaxSize(255)"`
34-
Website string `binding:"Url;MaxSize(255)"`
34+
Website string `binding:"ValidUrl;MaxSize(255)"`
3535
Location string `binding:"MaxSize(50)"`
3636
MaxRepoCreation int
3737
}

modules/auth/repo_form.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) {
8787
type RepoSettingForm struct {
8888
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
8989
Description string `binding:"MaxSize(255)"`
90-
Website string `binding:"Url;MaxSize(255)"`
90+
Website string `binding:"ValidUrl;MaxSize(255)"`
9191
Interval string
9292
MirrorAddress string
9393
Private bool
@@ -143,7 +143,7 @@ func (f WebhookForm) ChooseEvents() bool {
143143

144144
// NewWebhookForm form for creating web hook
145145
type NewWebhookForm struct {
146-
PayloadURL string `binding:"Required;Url"`
146+
PayloadURL string `binding:"Required;ValidUrl"`
147147
ContentType int `binding:"Required"`
148148
Secret string
149149
WebhookForm
@@ -156,7 +156,7 @@ func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs binding.Errors) bin
156156

157157
// NewSlackHookForm form for creating slack hook
158158
type NewSlackHookForm struct {
159-
PayloadURL string `binding:"Required;Url"`
159+
PayloadURL string `binding:"Required;ValidUrl"`
160160
Channel string `binding:"Required"`
161161
Username string
162162
IconURL string
@@ -323,7 +323,7 @@ type EditRepoFileForm struct {
323323
CommitSummary string `binding:"MaxSize(100)"`
324324
CommitMessage string
325325
CommitChoice string `binding:"Required;MaxSize(50)"`
326-
NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
326+
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
327327
LastCommit string
328328
}
329329

@@ -356,7 +356,7 @@ type UploadRepoFileForm struct {
356356
CommitSummary string `binding:"MaxSize(100)"`
357357
CommitMessage string
358358
CommitChoice string `binding:"Required;MaxSize(50)"`
359-
NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
359+
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
360360
Files []string
361361
}
362362

@@ -387,7 +387,7 @@ type DeleteRepoFileForm struct {
387387
CommitSummary string `binding:"MaxSize(100)"`
388388
CommitMessage string
389389
CommitChoice string `binding:"Required;MaxSize(50)"`
390-
NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
390+
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
391391
}
392392

393393
// Validate validates the fields

modules/auth/user_form.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ type UpdateProfileForm struct {
103103
FullName string `binding:"MaxSize(100)"`
104104
Email string `binding:"Required;Email;MaxSize(254)"`
105105
KeepEmailPrivate bool
106-
Website string `binding:"Url;MaxSize(255)"`
106+
Website string `binding:"ValidUrl;MaxSize(255)"`
107107
Location string `binding:"MaxSize(50)"`
108108
}
109109

modules/validation/binding.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

modules/validation/binding_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)