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 testsrelease/v1.2
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