refactor: markup render (#38864)

1. add missing CSP header to api & web render endpoints.
2. make jupyter render skip post-processors, nothing to process
3. make ShortLinkProcessor correctly validate URL schemes and respect
the CustomURLSchemes setting
This commit is contained in:
wxiaoguang
2026-08-12 00:01:02 +08:00
committed by GitHub
parent eeeb5d7c7b
commit 938cde959e
25 changed files with 255 additions and 172 deletions
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"regexp"
"slices"
"strings"
"sync"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
"mvdan.cc/xurls/v2"
)
type globalVarsType struct {
schemeRegexp *regexp.Regexp // extract the scheme part from a link
wwwURLRegexp *regexp.Regexp // matching "www.{any-site}/{any-path}" pattern
LinkifyRegex *regexp.Regexp // fast matching a URL link (powered by "xurls" package with custom schemes), no any extra validation.
allowedSchemes []string // nil means "allow all" (but disable the unsafe ones)
DisallowedSchemes []string
}
const regexpScheme = `[a-zA-Z][-+.a-zA-Z0-9]*`
var GlobalVars = sync.OnceValue(func() *globalVarsType {
v := &globalVarsType{}
v.schemeRegexp = regexp.MustCompile(`^` + regexpScheme + `:`)
v.wwwURLRegexp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
v.LinkifyRegex, _ = xurls.StrictMatchingScheme("https?://")
v.allowedSchemes = []string{"http", "https"}
v.DisallowedSchemes = []string{"data", "javascript", "vbscript"}
return v
})
type CheckLinkURLSchemeResult struct {
HasScheme, AllowToLinkify bool
}
func CheckLinkURLScheme(link string) CheckLinkURLSchemeResult {
vars := GlobalVars()
m := vars.schemeRegexp.FindStringSubmatch(link)
if m == nil {
return CheckLinkURLSchemeResult{AllowToLinkify: true} // relative link is always valid
}
urlScheme := strings.ToLower(m[0])
urlScheme = urlScheme[0 : len(urlScheme)-1] // remove the trailing ":"
allowed := len(vars.allowedSchemes) == 0 || slices.Contains(vars.allowedSchemes, urlScheme)
disabled := slices.Contains(vars.DisallowedSchemes, urlScheme)
return CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: allowed && !disabled}
}
func InitLinkURLSchemes(customSchemes []string) {
validScheme := regexp.MustCompile(`^` + regexpScheme + `$`)
schemes := container.Set[string]{}
for _, scheme := range customSchemes {
schemeLower := strings.ToLower(scheme)
if !validScheme.MatchString(schemeLower) {
log.Error("Invalid custom URL scheme %q", scheme)
continue
}
schemes.Add(schemeLower)
}
// HINT: CUSTOM-URL-SCHEMES-ALLOW: setting custom means also allow them besides http/https, no custom means "allow all"
if len(schemes) > 0 {
schemes.AddMultiple("http", "https")
linkifyRegexps := make([]string, 0, len(schemes))
for _, s := range schemes.Values() {
s += util.Iif(slices.Contains(xurls.SchemesNoAuthority, s), ":", "://")
linkifyRegexps = append(linkifyRegexps, regexp.QuoteMeta(s))
}
GlobalVars().LinkifyRegex, _ = xurls.StrictMatchingScheme(strings.Join(linkifyRegexps, "|"))
GlobalVars().allowedSchemes = schemes.Values()
} else {
GlobalVars().LinkifyRegex, _ = xurls.StrictMatchingScheme("https?://") // only auto-linkify http and https
GlobalVars().allowedSchemes = nil
}
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestURLSchemes(t *testing.T) {
t.Run("NoCustomSchemes", func(t *testing.T) {
InitLinkURLSchemes(nil)
assert.True(t, GlobalVars().LinkifyRegex.MatchString("http://example.com"))
assert.True(t, GlobalVars().LinkifyRegex.MatchString("https://example.com"))
assert.False(t, GlobalVars().LinkifyRegex.MatchString("some-other://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{AllowToLinkify: true}, CheckLinkURLScheme("foo/:"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("HTTP://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("https://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("some-other:foo"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("any-other:bar"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: false}, CheckLinkURLScheme("javascript:void"))
})
t.Run("WithCustomSchemes", func(t *testing.T) {
InitLinkURLSchemes([]string{"Some-Other"})
assert.True(t, GlobalVars().LinkifyRegex.MatchString("http://example.com"))
assert.True(t, GlobalVars().LinkifyRegex.MatchString("https://example.com"))
assert.True(t, GlobalVars().LinkifyRegex.MatchString("some-other://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("http://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("HTTPS://example.com"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: true}, CheckLinkURLScheme("some-other:foo"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: false}, CheckLinkURLScheme("any-other:bar"))
assert.Equal(t, CheckLinkURLSchemeResult{HasScheme: true, AllowToLinkify: false}, CheckLinkURLScheme("JavaScript:void"))
})
}
+1 -16
View File
@@ -8,29 +8,14 @@ package common
import (
"bytes"
"regexp"
"sync"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"mvdan.cc/xurls/v2"
)
type GlobalVarsType struct {
wwwURLRegexp *regexp.Regexp
LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation.
}
var GlobalVars = sync.OnceValue(func() *GlobalVarsType {
v := &GlobalVarsType{}
v.wwwURLRegexp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
return v
})
type linkifyParser struct{}
var defaultLinkifyParser = &linkifyParser{}
@@ -72,7 +57,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
var protocol []byte
typ := ast.AutoLinkURL
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
m = GlobalVars().LinkRegex.FindSubmatchIndex(line)
m = GlobalVars().LinkifyRegex.FindSubmatchIndex(line)
}
if m == nil && bytes.HasPrefix(line, domainWWW) {
m = GlobalVars().wwwURLRegexp.FindSubmatchIndex(line)