mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00: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