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 teststokarchuk/v1.17
parent
941281ae12
commit
f42ec6120e
@ -0,0 +1,102 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/go-macaron/binding" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// ErrGitRefName is git reference name error
|
||||||
|
ErrGitRefName = "GitRefNameError" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// GitRefNamePattern is regular expression wirh unallowed characters in git reference name
|
||||||
|
GitRefNamePattern = regexp.MustCompile("[^\\d\\w-_\\./]") |
||||||
|
) |
||||||
|
|
||||||
|
// AddBindingRules adds additional binding rules
|
||||||
|
func AddBindingRules() { |
||||||
|
addGitRefNameBindingRule() |
||||||
|
addValidURLBindingRule() |
||||||
|
} |
||||||
|
|
||||||
|
func addGitRefNameBindingRule() { |
||||||
|
// Git refname validation rule
|
||||||
|
binding.AddRule(&binding.Rule{ |
||||||
|
IsMatch: func(rule string) bool { |
||||||
|
return strings.HasPrefix(rule, "GitRefName") |
||||||
|
}, |
||||||
|
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { |
||||||
|
str := fmt.Sprintf("%v", val) |
||||||
|
|
||||||
|
if GitRefNamePattern.MatchString(str) { |
||||||
|
errs.Add([]string{name}, ErrGitRefName, "GitRefName") |
||||||
|
return false, errs |
||||||
|
} |
||||||
|
// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
|
||||||
|
if strings.HasPrefix(str, "/") || strings.HasSuffix(str, "/") || |
||||||
|
strings.HasPrefix(str, ".") || strings.HasSuffix(str, ".") || |
||||||
|
strings.HasSuffix(str, ".lock") || |
||||||
|
strings.Contains(str, "..") || strings.Contains(str, "//") { |
||||||
|
errs.Add([]string{name}, ErrGitRefName, "GitRefName") |
||||||
|
return false, errs |
||||||
|
} |
||||||
|
|
||||||
|
return true, errs |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func addValidURLBindingRule() { |
||||||
|
// URL validation rule
|
||||||
|
binding.AddRule(&binding.Rule{ |
||||||
|
IsMatch: func(rule string) bool { |
||||||
|
return strings.HasPrefix(rule, "ValidUrl") |
||||||
|
}, |
||||||
|
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { |
||||||
|
str := fmt.Sprintf("%v", val) |
||||||
|
if len(str) != 0 { |
||||||
|
if u, err := url.ParseRequestURI(str); err != nil || |
||||||
|
(u.Scheme != "http" && u.Scheme != "https") || |
||||||
|
!validPort(portOnly(u.Host)) { |
||||||
|
errs.Add([]string{name}, binding.ERR_URL, "Url") |
||||||
|
return false, errs |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true, errs |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func portOnly(hostport string) string { |
||||||
|
colon := strings.IndexByte(hostport, ':') |
||||||
|
if colon == -1 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
if i := strings.Index(hostport, "]:"); i != -1 { |
||||||
|
return hostport[i+len("]:"):] |
||||||
|
} |
||||||
|
if strings.Contains(hostport, "]") { |
||||||
|
return "" |
||||||
|
} |
||||||
|
return hostport[colon+len(":"):] |
||||||
|
} |
||||||
|
|
||||||
|
func validPort(p string) bool { |
||||||
|
for _, r := range []byte(p) { |
||||||
|
if r < '0' || r > '9' { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/http/httptest" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/go-macaron/binding" |
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
"gopkg.in/macaron.v1" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
testRoute = "/test" |
||||||
|
) |
||||||
|
|
||||||
|
type ( |
||||||
|
validationTestCase struct { |
||||||
|
description string |
||||||
|
data interface{} |
||||||
|
expectedErrors binding.Errors |
||||||
|
} |
||||||
|
|
||||||
|
handlerFunc func(interface{}, ...interface{}) macaron.Handler |
||||||
|
|
||||||
|
modeler interface { |
||||||
|
Model() string |
||||||
|
} |
||||||
|
|
||||||
|
TestForm struct { |
||||||
|
BranchName string `form:"BranchName" binding:"GitRefName"` |
||||||
|
URL string `form:"ValidUrl" binding:"ValidUrl"` |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
func performValidationTest(t *testing.T, testCase validationTestCase) { |
||||||
|
httpRecorder := httptest.NewRecorder() |
||||||
|
m := macaron.Classic() |
||||||
|
|
||||||
|
m.Post(testRoute, binding.Validate(testCase.data), func(actual binding.Errors) { |
||||||
|
assert.Equal(t, fmt.Sprintf("%+v", testCase.expectedErrors), fmt.Sprintf("%+v", actual)) |
||||||
|
}) |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", testRoute, nil) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
m.ServeHTTP(httpRecorder, req) |
||||||
|
|
||||||
|
switch httpRecorder.Code { |
||||||
|
case http.StatusNotFound: |
||||||
|
panic("Routing is messed up in test fixture (got 404): check methods and paths") |
||||||
|
case http.StatusInternalServerError: |
||||||
|
panic("Something bad happened on '" + testCase.description + "'") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,142 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/go-macaron/binding" |
||||||
|
) |
||||||
|
|
||||||
|
var gitRefNameValidationTestCases = []validationTestCase{ |
||||||
|
{ |
||||||
|
description: "Referece contains only characters", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "test", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name contains single slash", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "feature/test", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name contains backslash", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "feature\\test", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name starts with dot", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: ".test", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name ends with dot", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "test.", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name starts with slash", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "/test", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name ends with slash", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "test/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name ends with .lock", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "test.lock", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name contains multiple consecutive dots", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "te..st", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Reference name contains multiple consecutive slashes", |
||||||
|
data: TestForm{ |
||||||
|
BranchName: "te//st", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"BranchName"}, |
||||||
|
Classification: ErrGitRefName, |
||||||
|
Message: "GitRefName", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
func Test_GitRefNameValidation(t *testing.T) { |
||||||
|
AddBindingRules() |
||||||
|
|
||||||
|
for _, testCase := range gitRefNameValidationTestCases { |
||||||
|
t.Run(testCase.description, func(t *testing.T) { |
||||||
|
performValidationTest(t, testCase) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/go-macaron/binding" |
||||||
|
) |
||||||
|
|
||||||
|
var urlValidationTestCases = []validationTestCase{ |
||||||
|
{ |
||||||
|
description: "Empty URL", |
||||||
|
data: TestForm{ |
||||||
|
URL: "", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "URL without port", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://test.lan/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "URL with port", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://test.lan:3000/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "URL with IPv6 address without port", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://[::1]/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "URL with IPv6 address with port", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://[::1]:3000/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Invalid URL", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http//test.lan/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"URL"}, |
||||||
|
Classification: binding.ERR_URL, |
||||||
|
Message: "Url", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Invalid schema", |
||||||
|
data: TestForm{ |
||||||
|
URL: "ftp://test.lan/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"URL"}, |
||||||
|
Classification: binding.ERR_URL, |
||||||
|
Message: "Url", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Invalid port", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://test.lan:3x4/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"URL"}, |
||||||
|
Classification: binding.ERR_URL, |
||||||
|
Message: "Url", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
description: "Invalid port with IPv6 address", |
||||||
|
data: TestForm{ |
||||||
|
URL: "http://[::1]:3x4/", |
||||||
|
}, |
||||||
|
expectedErrors: binding.Errors{ |
||||||
|
binding.Error{ |
||||||
|
FieldNames: []string{"URL"}, |
||||||
|
Classification: binding.ERR_URL, |
||||||
|
Message: "Url", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ValidURLValidation(t *testing.T) { |
||||||
|
AddBindingRules() |
||||||
|
|
||||||
|
for _, testCase := range urlValidationTestCases { |
||||||
|
t.Run(testCase.description, func(t *testing.T) { |
||||||
|
performValidationTest(t, testCase) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue