mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 06:03:51 +03:00
Validate OAuth Redirect URIs (#32643)
This fixes a TODO in the code to validate the RedirectURIs when adding or editing an OAuth application in user settings. This also includes a refactor of the user settings tests to only create the DB once per top-level test to avoid reloading fixtures. (cherry picked from commit 16a7d343d78807e39df124756e5d43a69a2203a3) Conflicts: services/forms/user_form.go tests/integration/user_settings_test.go simple conflicts
This commit is contained in:
parent
3973f1022d
commit
2e00ae4cdd
6 changed files with 209 additions and 7 deletions
|
@ -41,6 +41,8 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
|
|||
|
||||
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
|
||||
func SplitTrimSpace(input, sep string) []string {
|
||||
// Trim initial leading & trailing space
|
||||
input = strings.TrimSpace(input)
|
||||
// replace CRLF with LF
|
||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"code.forgejo.org/go-chi/binding"
|
||||
"github.com/gobwas/glob"
|
||||
|
@ -33,6 +34,7 @@ const (
|
|||
// AddBindingRules adds additional binding rules
|
||||
func AddBindingRules() {
|
||||
addGitRefNameBindingRule()
|
||||
addValidURLListBindingRule()
|
||||
addValidURLBindingRule()
|
||||
addValidSiteURLBindingRule()
|
||||
addGlobPatternRule()
|
||||
|
@ -47,7 +49,7 @@ func addGitRefNameBindingRule() {
|
|||
// Git refname validation rule
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return strings.HasPrefix(rule, "GitRefName")
|
||||
return rule == "GitRefName"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
|
@ -61,11 +63,38 @@ func addGitRefNameBindingRule() {
|
|||
})
|
||||
}
|
||||
|
||||
func addValidURLListBindingRule() {
|
||||
// URL validation rule
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return rule == "ValidUrlList"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
if len(str) == 0 {
|
||||
errs.Add([]string{name}, binding.ERR_URL, "Url")
|
||||
return false, errs
|
||||
}
|
||||
|
||||
ok := true
|
||||
urls := util.SplitTrimSpace(str, "\n")
|
||||
for _, u := range urls {
|
||||
if !IsValidURL(u) {
|
||||
ok = false
|
||||
errs.Add([]string{name}, binding.ERR_URL, u)
|
||||
}
|
||||
}
|
||||
|
||||
return ok, errs
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func addValidURLBindingRule() {
|
||||
// URL validation rule
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return strings.HasPrefix(rule, "ValidUrl")
|
||||
return rule == "ValidUrl"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
|
@ -83,7 +112,7 @@ func addValidSiteURLBindingRule() {
|
|||
// URL validation rule
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return strings.HasPrefix(rule, "ValidSiteUrl")
|
||||
return rule == "ValidSiteUrl"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
|
@ -174,7 +203,7 @@ func addUsernamePatternRule() {
|
|||
func addValidGroupTeamMapRule() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return strings.HasPrefix(rule, "ValidGroupTeamMap")
|
||||
return rule == "ValidGroupTeamMap"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
_, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val))
|
||||
|
|
|
@ -27,6 +27,7 @@ type (
|
|||
TestForm struct {
|
||||
BranchName string `form:"BranchName" binding:"GitRefName"`
|
||||
URL string `form:"ValidUrl" binding:"ValidUrl"`
|
||||
URLs string `form:"ValidUrls" binding:"ValidUrlList"`
|
||||
GlobPattern string `form:"GlobPattern" binding:"GlobPattern"`
|
||||
RegexPattern string `form:"RegexPattern" binding:"RegexPattern"`
|
||||
}
|
||||
|
|
157
modules/validation/validurllist_test.go
Normal file
157
modules/validation/validurllist_test.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.forgejo.org/go-chi/binding"
|
||||
)
|
||||
|
||||
// This is a copy of all the URL tests cases, plus additional ones to
|
||||
// account for multiple URLs
|
||||
var urlListValidationTestCases = []validationTestCase{
|
||||
{
|
||||
description: "Empty URL",
|
||||
data: TestForm{
|
||||
URLs: "",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "URL without port",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan/",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "URL with port",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan:3000/",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "URL with IPv6 address without port",
|
||||
data: TestForm{
|
||||
URLs: "http://[::1]/",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "URL with IPv6 address with port",
|
||||
data: TestForm{
|
||||
URLs: "http://[::1]:3000/",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "Invalid URL",
|
||||
data: TestForm{
|
||||
URLs: "http//test.lan/",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "http//test.lan/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Invalid schema",
|
||||
data: TestForm{
|
||||
URLs: "ftp://test.lan/",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "ftp://test.lan/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Invalid port",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan:3x4/",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "http://test.lan:3x4/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Invalid port with IPv6 address",
|
||||
data: TestForm{
|
||||
URLs: "http://[::1]:3x4/",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "http://[::1]:3x4/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Multi URLs",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan:3000/\nhttp://test.local/",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "Multi URLs with newline",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan:3000/\nhttp://test.local/\n",
|
||||
},
|
||||
expectedErrors: binding.Errors{},
|
||||
},
|
||||
{
|
||||
description: "List with invalid entry",
|
||||
data: TestForm{
|
||||
URLs: "http://test.lan:3000/\nhttp://[::1]:3x4/",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "http://[::1]:3x4/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "List with two invalid entries",
|
||||
data: TestForm{
|
||||
URLs: "ftp://test.lan:3000/\nhttp://[::1]:3x4/\n",
|
||||
},
|
||||
expectedErrors: binding.Errors{
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "ftp://test.lan:3000/",
|
||||
},
|
||||
binding.Error{
|
||||
FieldNames: []string{"URLs"},
|
||||
Classification: binding.ERR_URL,
|
||||
Message: "http://[::1]:3x4/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_ValidURLListValidation(t *testing.T) {
|
||||
AddBindingRules()
|
||||
|
||||
for _, testCase := range urlListValidationTestCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
performValidationTest(t, testCase)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -47,7 +47,6 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO validate redirect URI
|
||||
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
|
@ -95,11 +94,25 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
|
|||
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
|
||||
|
||||
if ctx.HasError() {
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
|
||||
if err != nil {
|
||||
if auth.IsErrOAuthApplicationNotFound(err) {
|
||||
ctx.NotFound("Application not found", err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
if app.UID != oa.OwnerID {
|
||||
ctx.NotFound("Application not found", nil)
|
||||
return
|
||||
}
|
||||
ctx.Data["App"] = app
|
||||
|
||||
oa.renderEditPage(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO validate redirect URI
|
||||
var err error
|
||||
if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
|
||||
ID: ctx.ParamsInt64("id"),
|
||||
|
|
|
@ -388,7 +388,7 @@ func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
|
|||
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||
type EditOAuth2ApplicationForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
RedirectURIs string `binding:"Required" form:"redirect_uris"`
|
||||
RedirectURIs string `binding:"Required;ValidUrlList" form:"redirect_uris"`
|
||||
ConfidentialClient bool `form:"confidential_client"`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue