diff --git a/routers/common/redirect.go b/routers/common/redirect.go index d64f74ec82..e9f14a6eb1 100644 --- a/routers/common/redirect.go +++ b/routers/common/redirect.go @@ -16,11 +16,12 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) { // 2. when use "window.reload()", the hash is not respected, the newly loaded page won't scroll to the hash target. // 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. - redirect := req.PostFormValue("redirect") - if !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { - resp.WriteHeader(http.StatusBadRequest) + redirect := req.FormValue("redirect") + if req.Method != http.MethodPost || !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { + http.Error(resp, "Bad Request", http.StatusBadRequest) return } - resp.Header().Add("Location", redirect) + // no OpenRedirect, the "redirect" is validated by "IsCurrentGiteaSiteURL" above + resp.Header().Set("Location", redirect) resp.WriteHeader(http.StatusSeeOther) } diff --git a/routers/common/redirect_test.go b/routers/common/redirect_test.go new file mode 100644 index 0000000000..c4e6d2e6c7 --- /dev/null +++ b/routers/common/redirect_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestFetchRedirectDelegate(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://gitea/")() + + cases := []struct { + method string + input string + status int + }{ + {method: "POST", input: "/foo?k=v", status: http.StatusSeeOther}, + {method: "GET", input: "/foo?k=v", status: http.StatusBadRequest}, + {method: "POST", input: `\/foo?k=v`, status: http.StatusBadRequest}, + {method: "POST", input: `\\/foo?k=v`, status: http.StatusBadRequest}, + {method: "POST", input: "https://gitea/xxx", status: http.StatusSeeOther}, + {method: "POST", input: "https://other/xxx", status: http.StatusBadRequest}, + } + for _, c := range cases { + t.Run(c.method+" "+c.input, func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(c.method, "/?redirect="+url.QueryEscape(c.input), nil) + FetchRedirectDelegate(resp, req) + assert.Equal(t, c.status, resp.Code) + if c.status == http.StatusSeeOther { + assert.Equal(t, c.input, resp.Header().Get("Location")) + } else { + assert.Empty(t, resp.Header().Get("Location")) + assert.Equal(t, "Bad Request", strings.TrimSpace(resp.Body.String())) + } + }) + } +} diff --git a/web_src/js/external-render-helper.test.ts b/web_src/js/external-render-helper.test.ts new file mode 100644 index 0000000000..452d7f8f2d --- /dev/null +++ b/web_src/js/external-render-helper.test.ts @@ -0,0 +1,25 @@ +import './external-render-helper.ts'; + +test('isValidCssColor', async () => { + const isValidCssColor = window.testModules.externalRenderHelper!.isValidCssColor; + expect(isValidCssColor(null)).toBe(false); + expect(isValidCssColor('')).toBe(false); + + expect(isValidCssColor('#123')).toBe(true); + expect(isValidCssColor('#1234')).toBe(true); + expect(isValidCssColor('#abcabc')).toBe(true); + expect(isValidCssColor('#abcabc12')).toBe(true); + + expect(isValidCssColor('rgb(255 255 255)')).toBe(true); + expect(isValidCssColor('rgb(0, 255, 255)')).toBe(true); + + // examples from MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/rgb + expect(isValidCssColor('rgb(255 255 255 / 50%)')).toBe(true); + expect(isValidCssColor('rgb(from #123456 hwb(120deg 10% 20%) calc(g + 40) b / 0.5)')).toBe(true); + + expect(isValidCssColor('#123 ; other')).toBe(false); + expect(isValidCssColor('#123 : other')).toBe(false); + expect(isValidCssColor('#rgb(0, 255, 255); other')).toBe(false); + expect(isValidCssColor('#rgb(0, 255, 255)} other')).toBe(false); + expect(isValidCssColor('url(other)')).toBe(false); +}); diff --git a/web_src/js/external-render-helper.ts b/web_src/js/external-render-helper.ts index 3acac8db14..9162d0f550 100644 --- a/web_src/js/external-render-helper.ts +++ b/web_src/js/external-render-helper.ts @@ -15,6 +15,17 @@ RENDER_COMMAND = `echo '