mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Improve URL validation for external wiki and external issues (#4710)
* Improve URL validation for external wiki and external issues * Do not allow also localhost address for external URLs
This commit is contained in:
		| @@ -6,7 +6,6 @@ package validation | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" |  | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| @@ -70,13 +69,9 @@ func addValidURLBindingRule() { | |||||||
| 		}, | 		}, | ||||||
| 		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { | 		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { | ||||||
| 			str := fmt.Sprintf("%v", val) | 			str := fmt.Sprintf("%v", val) | ||||||
| 			if len(str) != 0 { | 			if len(str) != 0 && !IsValidURL(str) { | ||||||
| 				if u, err := url.ParseRequestURI(str); err != nil || | 				errs.Add([]string{name}, binding.ERR_URL, "Url") | ||||||
| 					(u.Scheme != "http" && u.Scheme != "https") || | 				return false, errs | ||||||
| 					!validPort(portOnly(u.Host)) { |  | ||||||
| 					errs.Add([]string{name}, binding.ERR_URL, "Url") |  | ||||||
| 					return false, errs |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return true, errs | 			return true, errs | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								modules/validation/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								modules/validation/helpers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | // Copyright 2018 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 ( | ||||||
|  | 	"net" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var loopbackIPBlocks []*net.IPNet | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	for _, cidr := range []string{ | ||||||
|  | 		"127.0.0.0/8", // IPv4 loopback | ||||||
|  | 		"::1/128",     // IPv6 loopback | ||||||
|  | 	} { | ||||||
|  | 		if _, block, err := net.ParseCIDR(cidr); err == nil { | ||||||
|  | 			loopbackIPBlocks = append(loopbackIPBlocks, block) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isLoopbackIP(ip string) bool { | ||||||
|  | 	pip := net.ParseIP(ip) | ||||||
|  | 	if pip == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	for _, block := range loopbackIPBlocks { | ||||||
|  | 		if block.Contains(pip) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsValidURL checks if URL is valid | ||||||
|  | func IsValidURL(uri string) bool { | ||||||
|  | 	if u, err := url.ParseRequestURI(uri); err != nil || | ||||||
|  | 		(u.Scheme != "http" && u.Scheme != "https") || | ||||||
|  | 		!validPort(portOnly(u.Host)) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsAPIURL checks if URL is current Gitea instance API URL | ||||||
|  | func IsAPIURL(uri string) bool { | ||||||
|  | 	return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsValidExternalURL checks if URL is valid external URL | ||||||
|  | func IsValidExternalURL(uri string) bool { | ||||||
|  | 	if !IsValidURL(uri) || IsAPIURL(uri) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u, err := url.ParseRequestURI(uri) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Currently check only if not loopback IP is provided to keep compatibility | ||||||
|  | 	if isLoopbackIP(u.Hostname()) || strings.ToLower(u.Hostname()) == "localhost" { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO: Later it should be added to allow local network IP addreses | ||||||
|  | 	//       only if allowed by special setting | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								modules/validation/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								modules/validation/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | // Copyright 2018 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/stretchr/testify/assert" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Test_IsValidURL(t *testing.T) { | ||||||
|  | 	cases := []struct { | ||||||
|  | 		description string | ||||||
|  | 		url         string | ||||||
|  | 		valid       bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description: "Empty URL", | ||||||
|  | 			url:         "", | ||||||
|  | 			valid:       false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Loobpack IPv4 URL", | ||||||
|  | 			url:         "http://127.0.1.1:5678/", | ||||||
|  | 			valid:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Loobpack IPv6 URL", | ||||||
|  | 			url:         "https://[::1]/", | ||||||
|  | 			valid:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Missing semicolon after schema", | ||||||
|  | 			url:         "http//meh/", | ||||||
|  | 			valid:       false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, testCase := range cases { | ||||||
|  | 		t.Run(testCase.description, func(t *testing.T) { | ||||||
|  | 			assert.Equal(t, testCase.valid, IsValidURL(testCase.url)) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_IsValidExternalURL(t *testing.T) { | ||||||
|  | 	setting.AppURL = "https://try.gitea.io/" | ||||||
|  |  | ||||||
|  | 	cases := []struct { | ||||||
|  | 		description string | ||||||
|  | 		url         string | ||||||
|  | 		valid       bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description: "Current instance URL", | ||||||
|  | 			url:         "https://try.gitea.io/test", | ||||||
|  | 			valid:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Loobpack IPv4 URL", | ||||||
|  | 			url:         "http://127.0.1.1:5678/", | ||||||
|  | 			valid:       false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Current instance API URL", | ||||||
|  | 			url:         "https://try.gitea.io/api/v1/user/follow", | ||||||
|  | 			valid:       false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Local network URL", | ||||||
|  | 			url:         "http://192.168.1.2/api/v1/user/follow", | ||||||
|  | 			valid:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "Local URL", | ||||||
|  | 			url:         "http://LOCALHOST:1234/whatever", | ||||||
|  | 			valid:       false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, testCase := range cases { | ||||||
|  | 		t.Run(testCase.description, func(t *testing.T) { | ||||||
|  | 			assert.Equal(t, testCase.valid, IsValidExternalURL(testCase.url)) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -987,6 +987,7 @@ settings.external_tracker_url = External Issue Tracker URL | |||||||
| settings.external_tracker_url_error = The external issue tracker URL is not a valid URL. | settings.external_tracker_url_error = The external issue tracker URL is not a valid URL. | ||||||
| settings.external_tracker_url_desc = Visitors are redirected to the external issue tracker URL when clicking on the issues tab. | settings.external_tracker_url_desc = Visitors are redirected to the external issue tracker URL when clicking on the issues tab. | ||||||
| settings.tracker_url_format = External Issue Tracker URL Format | settings.tracker_url_format = External Issue Tracker URL Format | ||||||
|  | settings.tracker_url_format_error = The external issue tracker URL format is not a valid URL. | ||||||
| settings.tracker_issue_style = External Issue Tracker Number Format | settings.tracker_issue_style = External Issue Tracker Number Format | ||||||
| settings.tracker_issue_style.numeric = Numeric | settings.tracker_issue_style.numeric = Numeric | ||||||
| settings.tracker_issue_style.alphanumeric = Alphanumeric | settings.tracker_issue_style.alphanumeric = Alphanumeric | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| // Copyright 2014 The Gogs Authors. All rights reserved. | // Copyright 2014 The Gogs Authors. All rights reserved. | ||||||
|  | // Copyright 2018 The Gitea Authors. All rights reserved. | ||||||
| // Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
| @@ -17,6 +18,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/modules/validation" | ||||||
| 	"code.gitea.io/gitea/routers/utils" | 	"code.gitea.io/gitea/routers/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -157,7 +159,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||||||
|  |  | ||||||
| 		if form.EnableWiki { | 		if form.EnableWiki { | ||||||
| 			if form.EnableExternalWiki { | 			if form.EnableExternalWiki { | ||||||
| 				if !strings.HasPrefix(form.ExternalWikiURL, "http://") && !strings.HasPrefix(form.ExternalWikiURL, "https://") { | 				if !validation.IsValidExternalURL(form.ExternalWikiURL) { | ||||||
| 					ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) | 					ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) | ||||||
| 					ctx.Redirect(repo.Link() + "/settings") | 					ctx.Redirect(repo.Link() + "/settings") | ||||||
| 					return | 					return | ||||||
| @@ -181,11 +183,16 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||||||
|  |  | ||||||
| 		if form.EnableIssues { | 		if form.EnableIssues { | ||||||
| 			if form.EnableExternalTracker { | 			if form.EnableExternalTracker { | ||||||
| 				if !strings.HasPrefix(form.ExternalTrackerURL, "http://") && !strings.HasPrefix(form.ExternalTrackerURL, "https://") { | 				if !validation.IsValidExternalURL(form.ExternalTrackerURL) { | ||||||
| 					ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) | 					ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) | ||||||
| 					ctx.Redirect(repo.Link() + "/settings") | 					ctx.Redirect(repo.Link() + "/settings") | ||||||
| 					return | 					return | ||||||
| 				} | 				} | ||||||
|  | 				if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalURL(form.TrackerURLFormat) { | ||||||
|  | 					ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) | ||||||
|  | 					ctx.Redirect(repo.Link() + "/settings") | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
| 				units = append(units, models.RepoUnit{ | 				units = append(units, models.RepoUnit{ | ||||||
| 					RepoID: repo.ID, | 					RepoID: repo.ID, | ||||||
| 					Type:   models.UnitTypeExternalTracker, | 					Type:   models.UnitTypeExternalTracker, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user