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

@@ -72,7 +72,7 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out
</head>
<body>
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
<script type="module" src="%s"></script>
<script nonce type="module" src="%s"></script>
</body>
</html>`,
public.AssetURI("css/swagger.css"),

View File

@@ -248,7 +248,7 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
extraLinkHref := ctx.RenderOptions.StandalonePageOptions.CurrentWebTheme.PublicAssetURI()
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
// DO NOT use "type=module", the script must run as early as possible, to set up the environment in the iframe
extraHeadHTML = htmlutil.HTMLFormat(`<script crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
extraHeadHTML = htmlutil.HTMLFormat(`<script nonce crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
}
ctx.usedByRender = true

View File

@@ -6,12 +6,10 @@ package templates
import (
"fmt"
"html"
"html/template"
"net/url"
"strconv"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/base"
@@ -69,8 +67,7 @@ func newFuncMapWebPage() template.FuncMap {
return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms"
},
"AssetURI": public.AssetURI,
"ScriptImport": scriptImport,
"AssetURI": public.AssetURI,
// -----------------------------------------------------------------
// setting
@@ -290,30 +287,3 @@ func QueryBuild(a ...any) template.URL {
}
return template.URL(s)
}
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 scriptImport(path string, typ ...string) template.HTML {
if len(typ) > 0 {
if typ[0] == "module" {
return template.HTML(`<script type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
}
panic("unsupported script type: " + typ[0])
}
return template.HTML(`<script src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
}

View File

@@ -6,11 +6,14 @@ package util
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
rand2 "math/rand/v2"
"slices"
"strconv"
"strings"
"sync"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@@ -85,10 +88,39 @@ func CryptoRandomString(length int64) (string, error) {
// CryptoRandomBytes generates `length` crypto bytes
// This differs from CryptoRandomString, as each byte in CryptoRandomString is generated by [0,61] range
// This function generates totally random bytes, each byte is generated by [0,255] range
// TODO: it never fails, remove the "error" in the future
func CryptoRandomBytes(length int64) ([]byte, error) {
buf := make([]byte, length)
_, err := rand.Read(buf)
return buf, err
if _, err := rand.Read(buf); err != nil {
panic(err) // this should never happen, "rand.Read" never fails
}
return buf, nil
}
var chaCha8RandPool = sync.OnceValue(func() *sync.Pool {
return &sync.Pool{
New: func() any {
var buf [32]byte
_, _ = rand.Read(buf[:])
return rand2.NewChaCha8(buf)
},
}
})
func FastCryptoRandomBytes(length int) []byte {
// ChaCha8 is about 20x times faster than system's crypto/rand.
// It is suitable for UUIDs, session IDs, etc
pool := chaCha8RandPool()
chaCha8Rand := pool.Get().(*rand2.ChaCha8)
defer pool.Put(chaCha8Rand)
buf := make([]byte, length)
_, _ = chaCha8Rand.Read(buf)
return buf
}
func FastCryptoRandomHex(length int) string {
buf := FastCryptoRandomBytes(length / 2)
return hex.EncodeToString(buf)
}
// ToLowerASCII returns s with all ASCII letters mapped to their lower case.