Skip to content

Commit bbffcc3

Browse files
authored
Multiple Escaping Improvements (#17551)
There are multiple places where Gitea does not properly escape URLs that it is building and there are multiple places where it builds urls when there is already a simpler function available to use this. This is an extensive PR attempting to fix these issues. 1. The first commit in this PR looks through all href, src and links in the Gitea codebase and has attempted to catch all the places where there is potentially incomplete escaping. 2. Whilst doing this we will prefer to use functions that create URLs over recreating them by hand. 3. All uses of strings should be directly escaped - even if they are not currently expected to contain escaping characters. The main benefit to doing this will be that we can consider relaxing the constraints on user names and reponames in future. 4. The next commit looks at escaping in the wiki and re-considers the urls that are used there. Using the improved escaping here wiki files containing '/'. (This implementation will currently still place all of the wiki files the root directory of the repo but this would not be difficult to change.) 5. The title generation in feeds is now properly escaped. 6. EscapePound is no longer needed - urls should be PathEscaped / QueryEscaped as necessary but then re-escaped with Escape when creating html with locales Signed-off-by: Andrew Thornton <[email protected]> Signed-off-by: Andrew Thornton <[email protected]>
1 parent 7e1ae38 commit bbffcc3

File tree

153 files changed

+891
-712
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+891
-712
lines changed

integrations/issue_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestViewIssuesSortByType(t *testing.T) {
6565
repo := unittest.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
6666

6767
session := loginUser(t, user.Name)
68-
req := NewRequest(t, "GET", repo.RelLink()+"/issues?type=created_by")
68+
req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by")
6969
resp := session.MakeRequest(t, req, http.StatusOK)
7070

7171
htmlDoc := NewHTMLParser(t, resp.Body)
@@ -97,7 +97,7 @@ func TestViewIssuesKeyword(t *testing.T) {
9797
issues.UpdateIssueIndexer(issue)
9898
time.Sleep(time.Second * 1)
9999
const keyword = "first"
100-
req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.RelLink(), keyword)
100+
req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword)
101101
resp := MakeRequest(t, req, http.StatusOK)
102102

103103
htmlDoc := NewHTMLParser(t, resp.Body)

integrations/links_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func testLinksAsUser(userName string, t *testing.T) {
156156
"/releases",
157157
"/releases/new",
158158
//"/wiki/_pages",
159-
"/wiki/_new",
159+
"/wiki/?action=_new",
160160
}
161161

162162
for _, repo := range apiRepos {

integrations/nonascii_branches_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ func TestNonasciiBranches(t *testing.T) {
6363
},
6464
{
6565
from: "ГлавнаяВетка",
66-
to: "branch/%d0%93%d0%bb%d0%b0%d0%b2%d0%bd%d0%b0%d1%8f%d0%92%d0%b5%d1%82%d0%ba%d0%b0",
66+
to: "branch/%D0%93%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F%D0%92%D0%B5%D1%82%D0%BA%D0%B0",
6767
status: http.StatusOK,
6868
},
6969
{
7070
from: "а/б/в",
71-
to: "branch/%d0%b0/%d0%b1/%d0%b2",
71+
to: "branch/%D0%B0/%D0%B1/%D0%B2",
7272
status: http.StatusOK,
7373
},
7474
{
7575
from: "Grüßen/README.md",
76-
to: "branch/Gr%c3%bc%c3%9fen/README.md",
76+
to: "branch/Gr%C3%BC%C3%9Fen/README.md",
7777
status: http.StatusOK,
7878
},
7979
{
@@ -83,7 +83,7 @@ func TestNonasciiBranches(t *testing.T) {
8383
},
8484
{
8585
from: "Plus+Is+Not+Space/Файл.md",
86-
to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md",
86+
to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
8787
status: http.StatusOK,
8888
},
8989
{
@@ -93,28 +93,28 @@ func TestNonasciiBranches(t *testing.T) {
9393
},
9494
{
9595
from: "ブランチ",
96-
to: "branch/%e3%83%96%e3%83%a9%e3%83%b3%e3%83%81",
96+
to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
9797
status: http.StatusOK,
9898
},
9999
// Tags
100100
{
101101
from: "Тэг",
102-
to: "tag/%d0%a2%d1%8d%d0%b3",
102+
to: "tag/%D0%A2%D1%8D%D0%B3",
103103
status: http.StatusOK,
104104
},
105105
{
106106
from: "Ё/人",
107-
to: "tag/%d0%81/%e4%ba%ba",
107+
to: "tag/%D0%81/%E4%BA%BA",
108108
status: http.StatusOK,
109109
},
110110
{
111111
from: "タグ",
112-
to: "tag/%e3%82%bf%e3%82%b0",
112+
to: "tag/%E3%82%BF%E3%82%B0",
113113
status: http.StatusOK,
114114
},
115115
{
116116
from: "タグ/ファイル.md",
117-
to: "tag/%e3%82%bf%e3%82%b0/%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab.md",
117+
to: "tag/%E3%82%BF%E3%82%B0/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md",
118118
status: http.StatusOK,
119119
},
120120
// Files
@@ -125,38 +125,38 @@ func TestNonasciiBranches(t *testing.T) {
125125
},
126126
{
127127
from: "Файл.md",
128-
to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md",
128+
to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
129129
status: http.StatusOK,
130130
},
131131
{
132132
from: "ファイル.md",
133-
to: "branch/Plus+Is+Not+Space/%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab.md",
133+
to: "branch/Plus+Is+Not+Space/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md",
134134
status: http.StatusNotFound, // it's not on default branch
135135
},
136136
// Same but url-encoded (few tests)
137137
{
138138
from: "%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
139-
to: "branch/%e3%83%96%e3%83%a9%e3%83%b3%e3%83%81",
139+
to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
140140
status: http.StatusOK,
141141
},
142142
{
143143
from: "%E3%82%BF%E3%82%b0",
144-
to: "tag/%e3%82%bf%e3%82%b0",
144+
to: "tag/%E3%82%BF%E3%82%B0",
145145
status: http.StatusOK,
146146
},
147147
{
148148
from: "%D0%A4%D0%B0%D0%B9%D0%BB.md",
149-
to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md",
149+
to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
150150
status: http.StatusOK,
151151
},
152152
{
153153
from: "%D0%81%2F%E4%BA%BA",
154-
to: "tag/%d0%81/%e4%ba%ba",
154+
to: "tag/%D0%81/%E4%BA%BA",
155155
status: http.StatusOK,
156156
},
157157
{
158158
from: "Ё%2F%E4%BA%BA",
159-
to: "tag/%d0%81/%e4%ba%ba",
159+
to: "tag/%D0%81/%E4%BA%BA",
160160
status: http.StatusOK,
161161
},
162162
}

models/action.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package models
77

88
import (
99
"fmt"
10+
"net/url"
1011
"path"
1112
"strconv"
1213
"strings"
@@ -185,10 +186,8 @@ func (a *Action) ShortRepoPath() string {
185186

186187
// GetRepoLink returns relative link to action repository.
187188
func (a *Action) GetRepoLink() string {
188-
if len(setting.AppSubURL) > 0 {
189-
return path.Join(setting.AppSubURL, a.GetRepoPath())
190-
}
191-
return "/" + a.GetRepoPath()
189+
// path.Join will skip empty strings
190+
return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName()), url.PathEscape(a.GetRepoName()))
192191
}
193192

194193
// GetRepositoryFromMatch returns a *Repository from a username and repo strings

models/attachment.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package models
77
import (
88
"context"
99
"fmt"
10+
"net/url"
1011
"path"
1112

1213
"code.gitea.io/gitea/models/db"
@@ -59,7 +60,7 @@ func (a *Attachment) RelativePath() string {
5960

6061
// DownloadURL returns the download url of the attached file
6162
func (a *Attachment) DownloadURL() string {
62-
return fmt.Sprintf("%sattachments/%s", setting.AppURL, a.UUID)
63+
return setting.AppURL + "attachments/" + url.PathEscape(a.UUID)
6364
}
6465

6566
// LinkedRepository returns the linked repo if any

models/avatars/avatar.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,15 @@ func GenerateUserAvatarFastLink(userName string, size int) string {
112112
if size < 0 {
113113
size = 0
114114
}
115-
return setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size)
115+
return setting.AppSubURL + "/user/avatar/" + url.PathEscape(userName) + "/" + strconv.Itoa(size)
116116
}
117117

118118
// GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}"
119119
func GenerateUserAvatarImageLink(userAvatar string, size int) string {
120120
if size > 0 {
121-
return setting.AppSubURL + "/avatars/" + userAvatar + "?size=" + strconv.Itoa(size)
121+
return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) + "?size=" + strconv.Itoa(size)
122122
}
123-
return setting.AppSubURL + "/avatars/" + userAvatar
123+
return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar)
124124
}
125125

126126
// generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy
@@ -155,7 +155,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
155155
return generateRecognizedAvatarURL(*avatarURL, size)
156156
}
157157
// for non-final link, we should return fast (use a 302 redirection link)
158-
urlStr := setting.AppSubURL + "/avatar/" + emailHash
158+
urlStr := setting.AppSubURL + "/avatar/" + url.PathEscape(emailHash)
159159
if size > 0 {
160160
urlStr += "?size=" + strconv.Itoa(size)
161161
}

models/commit_status.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package models
77
import (
88
"crypto/sha1"
99
"fmt"
10+
"net/url"
1011
"strings"
1112
"time"
1213

@@ -137,8 +138,7 @@ func (status *CommitStatus) loadAttributes(e db.Engine) (err error) {
137138
// APIURL returns the absolute APIURL to this commit-status.
138139
func (status *CommitStatus) APIURL() string {
139140
_ = status.loadAttributes(db.GetEngine(db.DefaultContext))
140-
return fmt.Sprintf("%sapi/v1/repos/%s/statuses/%s",
141-
setting.AppURL, status.Repo.FullName(), status.SHA)
141+
return status.Repo.APIURL() + "/statuses/" + url.PathEscape(status.SHA)
142142
}
143143

144144
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc

models/issue.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,17 @@ func (issue *Issue) HTMLURL() string {
372372
return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
373373
}
374374

375+
// Link returns the Link URL to this issue.
376+
func (issue *Issue) Link() string {
377+
var path string
378+
if issue.IsPull {
379+
path = "pulls"
380+
} else {
381+
path = "issues"
382+
}
383+
return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index)
384+
}
385+
375386
// DiffURL returns the absolute URL to this diff
376387
func (issue *Issue) DiffURL() string {
377388
if issue.IsPull {

models/notification.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package models
66

77
import (
88
"fmt"
9+
"net/url"
910
"strconv"
1011

1112
"code.gitea.io/gitea/models/db"
@@ -475,7 +476,7 @@ func (n *Notification) HTMLURL() string {
475476
}
476477
return n.Issue.HTMLURL()
477478
case NotificationSourceCommit:
478-
return n.Repository.HTMLURL() + "/commit/" + n.CommitID
479+
return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
479480
case NotificationSourceRepository:
480481
return n.Repository.HTMLURL()
481482
}

models/release.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import (
1010
"errors"
1111
"fmt"
1212
"sort"
13+
"strconv"
1314
"strings"
1415

1516
"code.gitea.io/gitea/models/db"
16-
"code.gitea.io/gitea/modules/setting"
1717
"code.gitea.io/gitea/modules/structs"
1818
"code.gitea.io/gitea/modules/timeutil"
1919
"code.gitea.io/gitea/modules/util"
@@ -78,23 +78,22 @@ func (r *Release) LoadAttributes() error {
7878

7979
// APIURL the api url for a release. release must have attributes loaded
8080
func (r *Release) APIURL() string {
81-
return fmt.Sprintf("%sapi/v1/repos/%s/releases/%d",
82-
setting.AppURL, r.Repo.FullName(), r.ID)
81+
return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10)
8382
}
8483

8584
// ZipURL the zip url for a release. release must have attributes loaded
8685
func (r *Release) ZipURL() string {
87-
return fmt.Sprintf("%s/archive/%s.zip", r.Repo.HTMLURL(), r.TagName)
86+
return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".zip"
8887
}
8988

9089
// TarURL the tar.gz url for a release. release must have attributes loaded
9190
func (r *Release) TarURL() string {
92-
return fmt.Sprintf("%s/archive/%s.tar.gz", r.Repo.HTMLURL(), r.TagName)
91+
return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".tar.gz"
9392
}
9493

9594
// HTMLURL the url for a release on the web UI. release must have attributes loaded
9695
func (r *Release) HTMLURL() string {
97-
return fmt.Sprintf("%s/releases/tag/%s", r.Repo.HTMLURL(), r.TagName)
96+
return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
9897
}
9998

10099
// IsReleaseExist returns true if release with given tag name already exists.

models/repo.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ func (repo *Repository) FullName() string {
314314

315315
// HTMLURL returns the repository HTML URL
316316
func (repo *Repository) HTMLURL() string {
317-
return setting.AppURL + repo.FullName()
317+
return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
318318
}
319319

320320
// CommitLink make link to by commit full ID
@@ -323,14 +323,14 @@ func (repo *Repository) CommitLink(commitID string) (result string) {
323323
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
324324
result = ""
325325
} else {
326-
result = repo.HTMLURL() + "/commit/" + commitID
326+
result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID)
327327
}
328328
return
329329
}
330330

331331
// APIURL returns the repository API URL
332332
func (repo *Repository) APIURL() string {
333-
return setting.AppURL + "api/v1/repos/" + repo.FullName()
333+
return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
334334
}
335335

336336
// GetCommitsCountCacheKey returns cache key used for commits count caching.
@@ -709,19 +709,14 @@ func (repo *Repository) GitConfigPath() string {
709709
return GitConfigPath(repo.RepoPath())
710710
}
711711

712-
// RelLink returns the repository relative link
713-
func (repo *Repository) RelLink() string {
714-
return "/" + repo.FullName()
715-
}
716-
717712
// Link returns the repository link
718713
func (repo *Repository) Link() string {
719-
return setting.AppSubURL + "/" + repo.FullName()
714+
return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
720715
}
721716

722717
// ComposeCompareURL returns the repository comparison URL
723718
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
724-
return fmt.Sprintf("%s/compare/%s...%s", repo.FullName(), oldCommitID, newCommitID)
719+
return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
725720
}
726721

727722
// UpdateDefaultBranch updates the default branch
@@ -930,11 +925,11 @@ func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
930925
}
931926

932927
if setting.SSH.Port != 22 {
933-
cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), repo.OwnerName, repoName)
928+
cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
934929
} else if setting.Repository.UseCompatSSHURI {
935-
cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, repo.OwnerName, repoName)
930+
cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
936931
} else {
937-
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, repo.OwnerName, repoName)
932+
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
938933
}
939934
cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
940935
return cl

models/repo_avatar.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"image/png"
1212
"io"
13+
"net/url"
1314
"strconv"
1415
"strings"
1516

@@ -96,7 +97,7 @@ func (repo *Repository) relAvatarLink(e db.Engine) string {
9697
return ""
9798
}
9899
}
99-
return setting.AppSubURL + "/repo-avatars/" + repo.Avatar
100+
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
100101
}
101102

102103
// AvatarLink returns a link to the repository's avatar.

0 commit comments

Comments
 (0)