"Redirect" functions should only redirect if the target is for current Gitea site.tags/v1.22.0-rc0
"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)) | |||||
} | } |
"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")) | |||||
} | } |
// 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 | ||||
} | } |
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 | ||||
} | } | ||||
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 | ||||
} | } | ||||
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 | ||||
} | } | ||||
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 { |
// 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 | ||||
} | } | ||||
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 | ||||
} | } | ||||