Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/markup/external/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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="not-needed" type="module" src="%s"></script>
</body>
</html>`,
public.AssetURI("css/swagger.css"),
Expand Down
2 changes: 1 addition & 1 deletion modules/markup/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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="not-needed" crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
}

ctx.usedByRender = true
Expand Down
32 changes: 1 addition & 31 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ package templates

import (
"fmt"
"html"
"html/template"
"net/url"
"strconv"
"strings"
"sync"
"time"

"code.gitea.io/gitea/modules/base"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
29 changes: 27 additions & 2 deletions modules/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -85,10 +88,32 @@ 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
Comment thread
wxiaoguang marked this conversation as resolved.
}
return buf, nil
}

var chaCha8Rand = sync.OnceValue(func() *rand2.ChaCha8 {
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
buf := make([]byte, length)
_, _ = chaCha8Rand().Read(buf)
return buf
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
}

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.
Expand Down
2 changes: 0 additions & 2 deletions services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions services/context/context_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -83,3 +90,47 @@ 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) {
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 {
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` +
`default-src *` + // allow all by default (the same as old releases with no CSP)
`script-src * 'nonce-` + c.CspScriptNonce() + `';` +
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
`style-src * 'unsafe-inline';` + // it seems that Vue needs it, need to investigate
`">`)
}
2 changes: 1 addition & 1 deletion templates/base/footer.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</div>
{{template "custom/body_outer_post" .}}
{{template "base/footer_content" .}}
{{ScriptImport "js/index.js" "module"}}
{{ctx.ScriptImport "js/index.js" "module"}}
{{template "custom/footer" .}}
</body>
</html>
1 change: 1 addition & 0 deletions templates/base/head.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
{{ctx.HeadMetaContentSecurityPolicy}}
Comment thread
wxiaoguang marked this conversation as resolved.
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
Expand Down
4 changes: 2 additions & 2 deletions templates/base/head_script.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
If you are customizing Gitea, please do not change this file.
If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
*/}}
<script>
<script nonce="{{ctx.CspScriptNonce}}">
{{/* before our JS code gets loaded, use arrays to store errors, then the arrays will be switched to our error handler later */}}
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
Expand Down Expand Up @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
window.config.pageData = window.config.pageData || {};
</script>
{{ScriptImport "js/iife.js"}}
{{ctx.ScriptImport "js/iife.js"}}
4 changes: 2 additions & 2 deletions templates/repo/diff/box.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}}
{{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}}
</button>
<script>
<script nonce="{{ctx.CspScriptNonce}}">
// Default to true if unset
const diffTreeVisible = window.localUserSettings.getBoolean('diff_file_tree_visible', true);
const diffTreeBtn = document.querySelector('.diff-toggle-file-tree-button');
Expand Down Expand Up @@ -62,7 +62,7 @@
{{if $showFileTree}}
{{$.FileIconPoolHTML}}
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
<script>
<script nonce="{{ctx.CspScriptNonce}}">
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
</script>
{{end}}
Expand Down
2 changes: 1 addition & 1 deletion templates/repo/issue/view_content/pull_merge_box.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
{{$hasPendingPullRequestMergeTip = ctx.Locale.Tr "repo.pulls.auto_merge_has_pending_schedule" .PendingPullRequestMerge.Doer.Name $createdPRMergeStr}}
{{end}}
<div class="divider"></div>
<script type="module">
<script nonce="{{ctx.CspScriptNonce}}" type="module">
(() => {
const defaultMergeTitle = {{.DefaultMergeMessage}};
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
Expand Down
2 changes: 1 addition & 1 deletion templates/shared/combomarkdowneditor.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
{{if .DisableAutosize}}data-disable-autosize="{{.DisableAutosize}}"{{end}}
>{{.TextareaContent}}</textarea>
</text-expander>
<script>
<script nonce="{{ctx.CspScriptNonce}}">
if (window.localUserSettings.getBoolean('markdown-editor-monospace')) {
document.querySelector('.markdown-text-editor').classList.add('tw-font-mono');
}
Expand Down
3 changes: 2 additions & 1 deletion templates/status/500.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
{{ctx.HeadMetaContentSecurityPolicy}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Internal Server Error - {{AppName}}</title>
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
Expand Down Expand Up @@ -52,7 +53,7 @@
{{/* When a sub-template triggers an 500 error, its parent template has been partially rendered, then the 500 page
will be rendered after that partially rendered page, the HTML/JS are totally broken. Use this inline script to try to move it to main viewport.
And this page shouldn't include any other JS file, avoid duplicate JS execution (still due to the partial rendering).*/}}
<script type="module">
<script nonce="{{ctx.CspScriptNonce}}" type="module">
const embedded = document.querySelector('.page-content .page-content.status-page-500');
if (embedded) {
// move the 500 error page content to main view
Expand Down
3 changes: 2 additions & 1 deletion templates/swagger/openapi-viewer.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{ctx.HeadMetaContentSecurityPolicy}}
<title>Gitea API</title>
{{/* HINT: SWAGGER-OPENAPI-VIEWER: another place is "modules/markup/external/openapi.go" */}}
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
Expand All @@ -11,6 +12,6 @@
<a class="swagger-back-link" href="{{AppSubUrl}}/">{{svg "octicon-reply"}}{{ctx.Locale.Tr "return_to_gitea"}}</a>
<div id="swagger-ui" data-source="{{AppSubUrl}}/swagger.v1.json"></div>
<footer class="page-footer"></footer>
{{ScriptImport "js/swagger.js" "module"}}
{{ctx.ScriptImport "js/swagger.js" "module"}}
</body>
</html>
6 changes: 3 additions & 3 deletions templates/user/auth/captcha.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
<div class="inline field tw-text-center required">
<div id="captcha" data-captcha-type="g-recaptcha" class="g-recaptcha-style" data-sitekey="{{.RecaptchaSitekey}}"></div>
</div>
<script defer src='{{.RecaptchaAPIScriptURL}}'></script>
<script nonce="{{ctx.CspScriptNonce}}" defer src='{{.RecaptchaAPIScriptURL}}'></script>
{{else if eq .CaptchaType "hcaptcha"}}
<div class="inline field tw-text-center required">
<div id="captcha" data-captcha-type="h-captcha" class="h-captcha-style" data-sitekey="{{.HcaptchaSitekey}}"></div>
</div>
<script defer src='https://hcaptcha.com/1/api.js'></script>
<script nonce="{{ctx.CspScriptNonce}}" defer src='https://hcaptcha.com/1/api.js'></script>
{{else if eq .CaptchaType "mcaptcha"}}
<div class="inline field tw-text-center">
<div class="m-captcha-style" id="mcaptcha__widget-container"></div>
Expand All @@ -25,5 +25,5 @@
<div class="inline field tw-text-center">
<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
</div>
<script defer src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
<script nonce="{{ctx.CspScriptNonce}}" defer src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
{{end}}{{end}}
2 changes: 1 addition & 1 deletion templates/user/dashboard/repolist.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script type="module">
<script nonce="{{ctx.CspScriptNonce}}" type="module">
const data = {
...window.config.pageData.dashboardRepoList, // it only contains searchLimit and uid

Expand Down
4 changes: 2 additions & 2 deletions tests/integration/markup_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) {
// default sandbox in sub page response
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy"))
// FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "<script>" tag, but it indeed is the sanitizer's job
assert.Equal(t, `<script crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><div><any attr="val">&lt;script&gt;&lt;/script&gt;</any></div>`, respSub.Body.String())
assert.Equal(t, `<script nonce="not-needed" crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><div><any attr="val">&lt;script&gt;&lt;/script&gt;</any></div>`, respSub.Body.String())
})
})

Expand All @@ -131,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) {
t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer")
respSub := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, `<script crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><script>foo("raw")</script>`, respSub.Body.String())
assert.Equal(t, `<script nonce="not-needed" crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><script>foo("raw")</script>`, respSub.Body.String())
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
})
})
Expand Down
2 changes: 2 additions & 0 deletions web_src/js/features/repo-issue-pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) {
}

function executeScripts(elem: HTMLElement) {
const scriptNonce = document.querySelector('head script[nonce]')!.getAttribute('nonce')!;
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
for (const oldScript of elem.querySelectorAll('script')) {
// TODO: that's the only way to load the data for the merge form. In the future
// we need to completely decouple the page data and embedded script
Expand All @@ -78,6 +79,7 @@ function executeScripts(elem: HTMLElement) {
if (attr.name === 'type' && attr.value === 'module') continue;
newScript.setAttribute(attr.name, attr.value);
}
newScript.setAttribute('nonce', scriptNonce);
newScript.text = oldScript.text;
document.body.append(newScript);
}
Expand Down
Loading