Skip to content
Merged
5 changes: 4 additions & 1 deletion custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
8 changes: 6 additions & 2 deletions modules/setting/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 "":
Expand Down
12 changes: 0 additions & 12 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
})
}
}
4 changes: 0 additions & 4 deletions routers/common/errpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions routers/common/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
22 changes: 19 additions & 3 deletions tests/integration/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
}