Use Content-Security-Policy: script nonce (#37232)

Fix #305
This commit is contained in:
wxiaoguang
2026-04-16 04:07:57 +08:00
committed by GitHub
parent 2644bb8490
commit 82bfde2a37
18 changed files with 134 additions and 52 deletions

View File

@@ -63,8 +63,6 @@ type Context struct {
Package *Package
}
type TemplateContext map[string]any
func init() {
web.RegisterResponseStatusProvider[*Base](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(BaseContextKey).(*Base)

View File

@@ -5,18 +5,25 @@ package context
import (
"context"
"fmt"
"html"
"html/template"
"net/http"
"strconv"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/webtheme"
)
type TemplateContext map[string]any
var _ context.Context = TemplateContext(nil)
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
@@ -83,3 +90,72 @@ func (c TemplateContext) AppFullLink(link ...string) template.URL {
}
return template.URL(s + strings.TrimPrefix(link[0], "/"))
}
var globalVars = sync.OnceValue(func() (ret struct {
scriptImportRemainingPart string
},
) {
// add onerror handler to alert users when the script fails to load:
// * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config
// * for developers: help them to remember to run "make watch-frontend" to build frontend assets
// the message will be directly put in the onerror JS code's string
onScriptErrorPrompt := `Please make sure the asset files can be accessed.`
if !setting.IsProd {
onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.`
}
onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt)
ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `"></script>`
return ret
})
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
if len(typ) > 0 {
if typ[0] == "module" {
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
}
panic("unsupported script type: " + typ[0])
}
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
}
func (c TemplateContext) CspScriptNonce() (ret string) {
// Generate a random nonce for each request and cache it in the context to make it usable during the whole rendering process.
//
// Some "<script>" tags are not in the CSP context, so they don't need nonce,
// these tags are written as "<script nonce>" to help developers to know that "no script nonce attribute is missing"
// (e.g.: when they grep the codebase for "script" tags)
ret, _ = c["_cspScriptNonce"].(string)
if ret == "" {
ret = util.FastCryptoRandomHex(32) // 16 bytes / 128 bits entropy
c["_cspScriptNonce"] = ret
}
return ret
}
func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
// The CSP problem is more complicated than it looks.
// Gitea was designed to support various "customizations", including:
// * custom themes (custom CSS and JS)
// * custom assets URL (CDN)
// * custom plugins and external renders (e.g.: PlantUML render, and the renders might also load some JS/CSS assets)
// There is no easy way for end users to make the CSP "source" completely right.
//
// There can be 2 approaches in the future:
// A. Let end users to configure their reverse proxy to add CSP header
// * Browsers will merge and use the stricter rules between Gitea and reverse proxy
// B. Introduce some config options in "app.ini"
// * Maybe this approach should be avoided, don't make the config system too complex, just let users use A
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` +
// allow all by default (the same as old releases with no CSP)
// "data:" is used to load the manifest in head (maybe also need to be refactored in the future)
// maybe some images are also loaded by "data:", need to investigate
`default-src * data:;` +
// enforce nonce for all scripts, disallow inline scripts
`script-src * 'nonce-` + c.CspScriptNonce() + `';` +
// it seems that Vue needs the unsafe-inline, and our custom colors (e.g.: label) also need it
`style-src * 'unsafe-inline';` +
`">`)
}