mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-13 15:40:56 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user