0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-06 17:08:27 +02:00

feat(security): set X-Content-Type-Options: nosniff by default (#37354)

Fixes #37316.

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Sai Asish Y 2026-04-24 04:21:34 -07:00 committed by GitHub
parent 1483291a87
commit 6826321570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 45 additions and 26 deletions

View File

@ -525,8 +525,11 @@ INTERNAL_TOKEN =
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
;TWO_FACTOR_AUTH =
;;
;; The value of the X-Frame-Options HTTP header for HTML responses. Use "unset" to remove the header.
;; The value of the X-Frame-Options HTTP header for all responses. Use "unset" to remove the header.
;X_FRAME_OPTIONS = SAMEORIGIN
;;
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
;X_CONTENT_TYPE_OPTIONS = nosniff
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -16,9 +16,11 @@ import (
// Security settings
var Security = struct {
// TODO: move more settings to this struct in future
XFrameOptions string
XFrameOptions string
XContentTypeOptions string
}{
XFrameOptions: "SAMEORIGIN",
XFrameOptions: "SAMEORIGIN",
XContentTypeOptions: "nosniff",
}
var (
@ -154,6 +156,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
}
Security.XContentTypeOptions = sec.Key("X_CONTENT_TYPE_OPTIONS").MustString(Security.XContentTypeOptions)
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
switch twoFactorAuth {
case "":

View File

@ -865,7 +865,6 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.BeforeRouting(securityHeaders())
if setting.CORSConfig.Enabled {
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
@ -1749,14 +1748,3 @@ func Routes() *web.Router {
return m
}
func securityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
// http://stackoverflow.com/a/3146618/244009
resp.Header().Set("x-content-type-options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}

View File

@ -33,10 +33,6 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in
}
httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
if setting.Security.XFrameOptions != "unset" {
w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
}
tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req))
w.WriteHeader(respCode)

View File

@ -28,6 +28,7 @@ func ProtocolMiddlewares() (handlers []any) {
// the order is important
handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths
handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery
handlers = append(handlers, SecurityHeadersHandler())
if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 {
handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies))
@ -48,6 +49,21 @@ func ProtocolMiddlewares() (handlers []any) {
return handlers
}
// SecurityHeadersHandler sets headers globally for every response that leaves Gitea.
func SecurityHeadersHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if setting.Security.XContentTypeOptions != "unset" {
resp.Header().Set("X-Content-Type-Options", setting.Security.XContentTypeOptions)
}
if setting.Security.XFrameOptions != "unset" {
resp.Header().Set("X-Frame-Options", setting.Security.XFrameOptions)
}
next.ServeHTTP(resp, req)
})
}
}
func RequestContextHandler() func(h http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {

View File

@ -196,10 +196,6 @@ func Contexter() func(next http.Handler) http.Handler {
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
if setting.Security.XFrameOptions != "unset" {
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
}
ctx.Data["SystemConfig"] = setting.Config()
ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth()

View File

@ -12,9 +12,14 @@ import (
"github.com/stretchr/testify/assert"
)
func TestRenderFileSVGIsInImgTag(t *testing.T) {
func TestView(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("RenderFileSVGIsInImgTag", testRenderFileSVGIsInImgTag)
t.Run("CommitListActions", testCommitListActions)
t.Run("SecurityHeadersDefaults", testSecurityHeadersDefaults)
}
func testRenderFileSVGIsInImgTag(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo2/src/branch/master/line.svg")
@ -26,8 +31,7 @@ func TestRenderFileSVGIsInImgTag(t *testing.T) {
assert.Equal(t, "/user2/repo2/raw/branch/master/line.svg", src)
}
func TestCommitListActions(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testCommitListActions(t *testing.T) {
session := loginUser(t, "user2")
t.Run("WikiRevisionList", func(t *testing.T) {
@ -65,3 +69,15 @@ func TestCommitListActions(t *testing.T) {
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true)
})
}
func testSecurityHeadersDefaults(t *testing.T) {
assertSecurityHeaders := func(t *testing.T, uri string) {
req := NewRequest(t, "GET", uri)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
assert.Equal(t, "SAMEORIGIN", resp.Header().Get("X-Frame-Options"))
}
assertSecurityHeaders(t, "/")
assertSecurityHeaders(t, "/api/v1/version")
assertSecurityHeaders(t, "/assets/img/favicon.png")
}