Skip to content

Commit 6826321

Browse files
SAY-5silverwindclaudewxiaoguang
authored
feat(security): set X-Content-Type-Options: nosniff by default (go-gitea#37354)
Fixes go-gitea#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>
1 parent 1483291 commit 6826321

7 files changed

Lines changed: 45 additions & 26 deletions

File tree

custom/conf/app.example.ini

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,11 @@ INTERNAL_TOKEN =
525525
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
526526
;TWO_FACTOR_AUTH =
527527
;;
528-
;; The value of the X-Frame-Options HTTP header for HTML responses. Use "unset" to remove the header.
528+
;; The value of the X-Frame-Options HTTP header for all responses. Use "unset" to remove the header.
529529
;X_FRAME_OPTIONS = SAMEORIGIN
530+
;;
531+
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
532+
;X_CONTENT_TYPE_OPTIONS = nosniff
530533

531534
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
532535
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/setting/security.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import (
1616
// Security settings
1717
var Security = struct {
1818
// TODO: move more settings to this struct in future
19-
XFrameOptions string
19+
XFrameOptions string
20+
XContentTypeOptions string
2021
}{
21-
XFrameOptions: "SAMEORIGIN",
22+
XFrameOptions: "SAMEORIGIN",
23+
XContentTypeOptions: "nosniff",
2224
}
2325

2426
var (
@@ -154,6 +156,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
154156
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
155157
}
156158

159+
Security.XContentTypeOptions = sec.Key("X_CONTENT_TYPE_OPTIONS").MustString(Security.XContentTypeOptions)
160+
157161
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
158162
switch twoFactorAuth {
159163
case "":

routers/api/v1/api.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,6 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
865865
func Routes() *web.Router {
866866
m := web.NewRouter()
867867

868-
m.BeforeRouting(securityHeaders())
869868
if setting.CORSConfig.Enabled {
870869
m.BeforeRouting(cors.Handler(cors.Options{
871870
AllowedOrigins: setting.CORSConfig.AllowDomain,
@@ -1749,14 +1748,3 @@ func Routes() *web.Router {
17491748

17501749
return m
17511750
}
1752-
1753-
func securityHeaders() func(http.Handler) http.Handler {
1754-
return func(next http.Handler) http.Handler {
1755-
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
1756-
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
1757-
// http://stackoverflow.com/a/3146618/244009
1758-
resp.Header().Set("x-content-type-options", "nosniff")
1759-
next.ServeHTTP(resp, req)
1760-
})
1761-
}
1762-
}

routers/common/errpage.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in
3333
}
3434

3535
httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
36-
if setting.Security.XFrameOptions != "unset" {
37-
w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
38-
}
39-
4036
tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req))
4137
w.WriteHeader(respCode)
4238

routers/common/middleware.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func ProtocolMiddlewares() (handlers []any) {
2828
// the order is important
2929
handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths
3030
handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery
31+
handlers = append(handlers, SecurityHeadersHandler())
3132

3233
if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 {
3334
handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies))
@@ -48,6 +49,21 @@ func ProtocolMiddlewares() (handlers []any) {
4849
return handlers
4950
}
5051

52+
// SecurityHeadersHandler sets headers globally for every response that leaves Gitea.
53+
func SecurityHeadersHandler() func(http.Handler) http.Handler {
54+
return func(next http.Handler) http.Handler {
55+
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
56+
if setting.Security.XContentTypeOptions != "unset" {
57+
resp.Header().Set("X-Content-Type-Options", setting.Security.XContentTypeOptions)
58+
}
59+
if setting.Security.XFrameOptions != "unset" {
60+
resp.Header().Set("X-Frame-Options", setting.Security.XFrameOptions)
61+
}
62+
next.ServeHTTP(resp, req)
63+
})
64+
}
65+
}
66+
5167
func RequestContextHandler() func(h http.Handler) http.Handler {
5268
return func(next http.Handler) http.Handler {
5369
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {

services/context/context.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,6 @@ func Contexter() func(next http.Handler) http.Handler {
196196

197197
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
198198

199-
if setting.Security.XFrameOptions != "unset" {
200-
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
201-
}
202-
203199
ctx.Data["SystemConfig"] = setting.Config()
204200

205201
ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth()

tests/integration/view_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import (
1212
"github.com/stretchr/testify/assert"
1313
)
1414

15-
func TestRenderFileSVGIsInImgTag(t *testing.T) {
15+
func TestView(t *testing.T) {
1616
defer tests.PrepareTestEnv(t)()
17+
t.Run("RenderFileSVGIsInImgTag", testRenderFileSVGIsInImgTag)
18+
t.Run("CommitListActions", testCommitListActions)
19+
t.Run("SecurityHeadersDefaults", testSecurityHeadersDefaults)
20+
}
1721

22+
func testRenderFileSVGIsInImgTag(t *testing.T) {
1823
session := loginUser(t, "user2")
1924

2025
req := NewRequest(t, "GET", "/user2/repo2/src/branch/master/line.svg")
@@ -26,8 +31,7 @@ func TestRenderFileSVGIsInImgTag(t *testing.T) {
2631
assert.Equal(t, "/user2/repo2/raw/branch/master/line.svg", src)
2732
}
2833

29-
func TestCommitListActions(t *testing.T) {
30-
defer tests.PrepareTestEnv(t)()
34+
func testCommitListActions(t *testing.T) {
3135
session := loginUser(t, "user2")
3236

3337
t.Run("WikiRevisionList", func(t *testing.T) {
@@ -65,3 +69,15 @@ func TestCommitListActions(t *testing.T) {
6569
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true)
6670
})
6771
}
72+
73+
func testSecurityHeadersDefaults(t *testing.T) {
74+
assertSecurityHeaders := func(t *testing.T, uri string) {
75+
req := NewRequest(t, "GET", uri)
76+
resp := MakeRequest(t, req, http.StatusOK)
77+
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
78+
assert.Equal(t, "SAMEORIGIN", resp.Header().Get("X-Frame-Options"))
79+
}
80+
assertSecurityHeaders(t, "/")
81+
assertSecurityHeaders(t, "/api/v1/version")
82+
assertSecurityHeaders(t, "/assets/img/favicon.png")
83+
}

0 commit comments

Comments
 (0)