Browse Source

Refactor URL detection (#29960)

"Redirect" functions should only redirect if the target is for current Gitea site.
tags/v1.22.0-rc0
wxiaoguang 3 months ago
parent
commit
01500957c2
No account linked to committer's email address

+ 27
- 7
modules/httplib/url.go View File

"strings" "strings"


"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
) )


// IsRiskyRedirectURL returns true if the URL is considered risky for redirects
func IsRiskyRedirectURL(s string) bool {
func urlIsRelative(s string, u *url.URL) bool {
// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH" // Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
// Therefore we should ignore these redirect locations to prevent open redirects // Therefore we should ignore these redirect locations to prevent open redirects
if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') { if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
return true
return false
} }
return u != nil && u.Scheme == "" && u.Host == ""
}


// IsRelativeURL detects if a URL is relative (no scheme or host)
func IsRelativeURL(s string) bool {
u, err := url.Parse(s) u, err := url.Parse(s)
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) {
return true
}
return err == nil && urlIsRelative(s, u)
}


return false
func IsCurrentGiteaSiteURL(s string) bool {
u, err := url.Parse(s)
if err != nil {
return false
}
if u.Path != "" {
u.Path = "/" + util.PathJoinRelX(u.Path)
if !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
}
if urlIsRelative(s, u) {
return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/"))
}
if u.Path == "" {
u.Path = "/"
}
return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL))
} }

+ 55
- 22
modules/httplib/url_test.go View File

"testing" "testing"


"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"


"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )


func TestIsRiskyRedirectURL(t *testing.T) {
setting.AppURL = "http://localhost:3000/"
tests := []struct {
input string
want bool
}{
{"", false},
{"foo", false},
{"/", false},
{"/foo?k=%20#abc", false},
func TestIsRelativeURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
rel := []string{
"",
"foo",
"/",
"/foo?k=%20#abc",
}
for _, s := range rel {
assert.True(t, IsRelativeURL(s), "rel = %q", s)
}
abs := []string{
"//",
"\\\\",
"/\\",
"\\/",
"mailto:a@b.com",
"https://test.com",
}
for _, s := range abs {
assert.False(t, IsRelativeURL(s), "abs = %q", s)
}
}


{"//", true},
{"\\\\", true},
{"/\\", true},
{"\\/", true},
{"mail:a@b.com", true},
{"https://test.com", true},
{setting.AppURL + "/foo", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input))
})
func TestIsCurrentGiteaSiteURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
good := []string{
"?key=val",
"/sub",
"/sub/",
"/sub/foo",
"/sub/foo/",
"http://localhost:3000/sub?key=val",
"http://localhost:3000/sub/",
} }
for _, s := range good {
assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s)
}
bad := []string{
"/",
"//",
"\\\\",
"/foo",
"http://localhost:3000/sub/..",
"http://localhost:3000/other",
"http://other/",
}
for _, s := range bad {
assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s)
}

setting.AppURL = "http://localhost:3000/"
setting.AppSubURL = ""
assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val"))
} }

+ 1
- 1
routers/common/redirect.go View File

// The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2",
// then frontend needs this delegate to redirect to the new location with hash correctly. // then frontend needs this delegate to redirect to the new location with hash correctly.
redirect := req.PostFormValue("redirect") redirect := req.PostFormValue("redirect")
if httplib.IsRiskyRedirectURL(redirect) {
if !httplib.IsCurrentGiteaSiteURL(redirect) {
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
return return
} }

+ 3
- 3
routers/web/auth/auth.go View File

if setting.LandingPageURL == setting.LandingPageLogin { if setting.LandingPageURL == setting.LandingPageLogin {
nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
} }
ctx.RedirectToFirst(redirectTo, nextRedirectTo)
ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
} }


func CheckAutoLogin(ctx *context.Context) bool { func CheckAutoLogin(ctx *context.Context) bool {
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
if obeyRedirect { if obeyRedirect {
ctx.RedirectToFirst(redirectTo)
ctx.RedirectToCurrentSite(redirectTo)
} }
return redirectTo return redirectTo
} }
ctx.Flash.Success(ctx.Tr("auth.account_activated")) ctx.Flash.Success(ctx.Tr("auth.account_activated"))
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToFirst(redirectTo)
ctx.RedirectToCurrentSite(redirectTo)
return return
} }



+ 1
- 1
routers/web/auth/oauth.go View File



if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToFirst(redirectTo)
ctx.RedirectToCurrentSite(redirectTo)
return return
} }



+ 1
- 1
routers/web/auth/password.go View File



if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToFirst(redirectTo)
ctx.RedirectToCurrentSite(redirectTo)
return return
} }



+ 1
- 1
routers/web/repo/repo.go View File

return return
} }


ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
} }


func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {

+ 1
- 1
routers/web/web.go View File



// Redirect to dashboard (or alternate location) if user tries to visit any non-login page. // Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
ctx.RedirectToFirst(ctx.FormString("redirect_to"))
ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"))
return return
} }



+ 3
- 3
services/context/context_response.go View File

ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
} }


// RedirectToFirst redirects to first not empty URL
func (ctx *Context) RedirectToFirst(location ...string) {
// RedirectToCurrentSite redirects to first not empty URL which belongs to current site
func (ctx *Context) RedirectToCurrentSite(location ...string) {
for _, loc := range location { for _, loc := range location {
if len(loc) == 0 { if len(loc) == 0 {
continue continue
} }


if httplib.IsRiskyRedirectURL(loc) {
if !httplib.IsCurrentGiteaSiteURL(loc) {
continue continue
} }



Loading…
Cancel
Save