Skip to content

Commit ad8cd4e

Browse files
committed
fix
1 parent 17f62bf commit ad8cd4e

18 files changed

Lines changed: 99 additions & 51 deletions

File tree

modules/markup/external/openapi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out
7272
</head>
7373
<body>
7474
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
75-
<script type="module" src="%s"></script>
75+
<script nonce="not-needed" type="module" src="%s"></script>
7676
</body>
7777
</html>`,
7878
public.AssetURI("css/swagger.css"),

modules/markup/render.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
248248
extraLinkHref := ctx.RenderOptions.StandalonePageOptions.CurrentWebTheme.PublicAssetURI()
249249
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
250250
// DO NOT use "type=module", the script must run as early as possible, to set up the environment in the iframe
251-
extraHeadHTML = htmlutil.HTMLFormat(`<script crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
251+
extraHeadHTML = htmlutil.HTMLFormat(`<script nonce="not-needed" crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
252252
}
253253

254254
ctx.usedByRender = true

modules/templates/helper.go

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ package templates
66

77
import (
88
"fmt"
9-
"html"
109
"html/template"
1110
"net/url"
1211
"strconv"
1312
"strings"
14-
"sync"
1513
"time"
1614

1715
"code.gitea.io/gitea/modules/base"
@@ -69,8 +67,7 @@ func newFuncMapWebPage() template.FuncMap {
6967
return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms"
7068
},
7169

72-
"AssetURI": public.AssetURI,
73-
"ScriptImport": scriptImport,
70+
"AssetURI": public.AssetURI,
7471

7572
// -----------------------------------------------------------------
7673
// setting
@@ -290,30 +287,3 @@ func QueryBuild(a ...any) template.URL {
290287
}
291288
return template.URL(s)
292289
}
293-
294-
var globalVars = sync.OnceValue(func() (ret struct {
295-
scriptImportRemainingPart string
296-
},
297-
) {
298-
// add onerror handler to alert users when the script fails to load:
299-
// * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config
300-
// * for developers: help them to remember to run "make watch-frontend" to build frontend assets
301-
// the message will be directly put in the onerror JS code's string
302-
onScriptErrorPrompt := `Please make sure the asset files can be accessed.`
303-
if !setting.IsProd {
304-
onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.`
305-
}
306-
onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt)
307-
ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `"></script>`
308-
return ret
309-
})
310-
311-
func scriptImport(path string, typ ...string) template.HTML {
312-
if len(typ) > 0 {
313-
if typ[0] == "module" {
314-
return template.HTML(`<script type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
315-
}
316-
panic("unsupported script type: " + typ[0])
317-
}
318-
return template.HTML(`<script src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
319-
}

modules/util/util.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ package util
66
import (
77
"bytes"
88
"crypto/rand"
9+
"encoding/hex"
910
"fmt"
1011
"math/big"
12+
rand2 "math/rand/v2"
1113
"slices"
1214
"strconv"
1315
"strings"
16+
"sync"
1417

1518
"golang.org/x/text/cases"
1619
"golang.org/x/text/language"
@@ -85,10 +88,32 @@ func CryptoRandomString(length int64) (string, error) {
8588
// CryptoRandomBytes generates `length` crypto bytes
8689
// This differs from CryptoRandomString, as each byte in CryptoRandomString is generated by [0,61] range
8790
// This function generates totally random bytes, each byte is generated by [0,255] range
91+
// TODO: it never fails, remove the "error" in the future
8892
func CryptoRandomBytes(length int64) ([]byte, error) {
8993
buf := make([]byte, length)
90-
_, err := rand.Read(buf)
91-
return buf, err
94+
if _, err := rand.Read(buf); err != nil {
95+
panic(err) // this should never happen, "rand.Read" never fails
96+
}
97+
return buf, nil
98+
}
99+
100+
var chaCha8Rand = sync.OnceValue(func() *rand2.ChaCha8 {
101+
var buf [32]byte
102+
_, _ = rand.Read(buf[:])
103+
return rand2.NewChaCha8(buf)
104+
})
105+
106+
func FastCryptoRandomBytes(length int) []byte {
107+
// ChaCha8 is about 20x times faster than system's crypto/rand.
108+
// It is suitable for UUIDs, session IDs, etc
109+
buf := make([]byte, length)
110+
_, _ = chaCha8Rand().Read(buf)
111+
return buf
112+
}
113+
114+
func FastCryptoRandomHex(length int) string {
115+
buf := FastCryptoRandomBytes(length / 2)
116+
return hex.EncodeToString(buf)
92117
}
93118

94119
// ToLowerASCII returns s with all ASCII letters mapped to their lower case.

routers/web/repo/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
322322
}
323323
}
324324
_ = clearFilesCommitInfo
325-
// clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
325+
clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
326326
}
327327
}
328328

services/context/context.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ type Context struct {
6363
Package *Package
6464
}
6565

66-
type TemplateContext map[string]any
67-
6866
func init() {
6967
web.RegisterResponseStatusProvider[*Base](func(req *http.Request) web_types.ResponseStatusProvider {
7068
return req.Context().Value(BaseContextKey).(*Base)

services/context/context_template.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ package context
55

66
import (
77
"context"
8+
"fmt"
9+
"html"
810
"html/template"
911
"net/http"
1012
"strconv"
1113
"strings"
14+
"sync"
1215
"time"
1316

1417
"code.gitea.io/gitea/modules/httplib"
18+
"code.gitea.io/gitea/modules/public"
1519
"code.gitea.io/gitea/modules/setting"
20+
"code.gitea.io/gitea/modules/util"
1621
"code.gitea.io/gitea/modules/web/middleware"
1722
"code.gitea.io/gitea/services/webtheme"
1823
)
1924

25+
type TemplateContext map[string]any
26+
2027
var _ context.Context = TemplateContext(nil)
2128

2229
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
@@ -83,3 +90,46 @@ func (c TemplateContext) AppFullLink(link ...string) template.URL {
8390
}
8491
return template.URL(s + strings.TrimPrefix(link[0], "/"))
8592
}
93+
94+
var globalVars = sync.OnceValue(func() (ret struct {
95+
scriptImportRemainingPart string
96+
},
97+
) {
98+
// add onerror handler to alert users when the script fails to load:
99+
// * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config
100+
// * for developers: help them to remember to run "make watch-frontend" to build frontend assets
101+
// the message will be directly put in the onerror JS code's string
102+
onScriptErrorPrompt := `Please make sure the asset files can be accessed.`
103+
if !setting.IsProd {
104+
onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.`
105+
}
106+
onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt)
107+
ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `"></script>`
108+
return ret
109+
})
110+
111+
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
112+
if len(typ) > 0 {
113+
if typ[0] == "module" {
114+
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
115+
}
116+
panic("unsupported script type: " + typ[0])
117+
}
118+
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
119+
}
120+
121+
func (c TemplateContext) CspScriptNonce() (ret string) {
122+
ret, _ = c["_cspScriptNonce"].(string)
123+
if ret == "" {
124+
ret = util.FastCryptoRandomHex(32) // 16 bytes / 128 bits entropy
125+
c["_cspScriptNonce"] = ret
126+
}
127+
return ret
128+
}
129+
130+
func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
131+
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` +
132+
`script-src * 'nonce-` + c.CspScriptNonce() + `';` +
133+
`style-src * 'unsafe-inline';` +
134+
`">`)
135+
}

templates/base/footer.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</div>
1010
{{template "custom/body_outer_post" .}}
1111
{{template "base/footer_content" .}}
12-
{{ScriptImport "js/index.js" "module"}}
12+
{{ctx.ScriptImport "js/index.js" "module"}}
1313
{{template "custom/footer" .}}
1414
</body>
1515
</html>

templates/base/head.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
33
<head>
4+
{{ctx.HeadMetaContentSecurityPolicy}}
45
<meta name="viewport" content="width=device-width, initial-scale=1">
56
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
67
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}

templates/base/head_script.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
If you are customizing Gitea, please do not change this file.
33
If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
44
*/}}
5-
<script>
5+
<script nonce="{{ctx.CspScriptNonce}}">
66
{{/* before our JS code gets loaded, use arrays to store errors, then the arrays will be switched to our error handler later */}}
77
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
88
window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
@@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
3131
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
3232
window.config.pageData = window.config.pageData || {};
3333
</script>
34-
{{ScriptImport "js/iife.js"}}
34+
{{ctx.ScriptImport "js/iife.js"}}

0 commit comments

Comments
 (0)