mirror of
https://github.com/go-gitea/gitea
synced 2024-11-11 04:21:21 +01:00
Backport #31192 by wxiaoguang Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
1987c86f3c
commit
b6280f4d21
8 changed files with 270 additions and 245 deletions
|
@ -156,13 +156,18 @@ func TestRender_links(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
}
|
}
|
||||||
// Text that should be turned into URL
|
|
||||||
|
|
||||||
defaultCustom := setting.Markdown.CustomURLSchemes
|
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||||
|
markup.ResetDefaultSanitizerForTesting()
|
||||||
|
defer func() {
|
||||||
|
setting.Markdown.CustomURLSchemes = oldCustomURLSchemes
|
||||||
|
markup.ResetDefaultSanitizerForTesting()
|
||||||
|
markup.CustomLinkURLSchemes(oldCustomURLSchemes)
|
||||||
|
}()
|
||||||
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
||||||
markup.InitializeSanitizer()
|
|
||||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||||
|
|
||||||
|
// Text that should be turned into URL
|
||||||
test(
|
test(
|
||||||
"https://www.example.com",
|
"https://www.example.com",
|
||||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
||||||
|
@ -246,11 +251,6 @@ func TestRender_links(t *testing.T) {
|
||||||
test(
|
test(
|
||||||
"ftps://gitea.com",
|
"ftps://gitea.com",
|
||||||
`<p>ftps://gitea.com</p>`)
|
`<p>ftps://gitea.com</p>`)
|
||||||
|
|
||||||
// Restore previous settings
|
|
||||||
setting.Markdown.CustomURLSchemes = defaultCustom
|
|
||||||
markup.InitializeSanitizer()
|
|
||||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_email(t *testing.T) {
|
func TestRender_email(t *testing.T) {
|
||||||
|
|
|
@ -46,7 +46,6 @@ func Init(ph *ProcessorHelper) {
|
||||||
DefaultProcessorHelper = *ph
|
DefaultProcessorHelper = *ph
|
||||||
}
|
}
|
||||||
|
|
||||||
NewSanitizer()
|
|
||||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,9 @@
|
||||||
package markup
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,211 +17,35 @@ type Sanitizer struct {
|
||||||
defaultPolicy *bluemonday.Policy
|
defaultPolicy *bluemonday.Policy
|
||||||
descriptionPolicy *bluemonday.Policy
|
descriptionPolicy *bluemonday.Policy
|
||||||
rendererPolicies map[string]*bluemonday.Policy
|
rendererPolicies map[string]*bluemonday.Policy
|
||||||
init sync.Once
|
allowAllRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sanitizer = &Sanitizer{}
|
defaultSanitizer *Sanitizer
|
||||||
allowAllRegex = regexp.MustCompile(".+")
|
defaultSanitizerOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
func GetDefaultSanitizer() *Sanitizer {
|
||||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
defaultSanitizerOnce.Do(func() {
|
||||||
// entire application lifecycle.
|
defaultSanitizer = &Sanitizer{
|
||||||
func NewSanitizer() {
|
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||||
sanitizer.init.Do(func() {
|
allowAllRegex: regexp.MustCompile(".+"),
|
||||||
InitializeSanitizer()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
|
|
||||||
func InitializeSanitizer() {
|
|
||||||
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
|
||||||
sanitizer.defaultPolicy = createDefaultPolicy()
|
|
||||||
sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
|
|
||||||
|
|
||||||
for name, renderer := range renderers {
|
for name, renderer := range renderers {
|
||||||
sanitizerRules := renderer.SanitizerRules()
|
sanitizerRules := renderer.SanitizerRules()
|
||||||
if len(sanitizerRules) > 0 {
|
if len(sanitizerRules) > 0 {
|
||||||
policy := createDefaultPolicy()
|
policy := defaultSanitizer.createDefaultPolicy()
|
||||||
addSanitizerRules(policy, sanitizerRules)
|
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||||
sanitizer.rendererPolicies[name] = policy
|
defaultSanitizer.rendererPolicies[name] = policy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||||
|
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||||
|
})
|
||||||
|
return defaultSanitizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDefaultPolicy() *bluemonday.Policy {
|
func ResetDefaultSanitizerForTesting() {
|
||||||
policy := bluemonday.UGCPolicy()
|
defaultSanitizer = nil
|
||||||
|
defaultSanitizerOnce = sync.Once{}
|
||||||
// For JS code copy and Mermaid loading state
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
|
||||||
|
|
||||||
// For code preview
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
|
||||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
|
||||||
|
|
||||||
// For code preview (unicode escape)
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
|
||||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
|
||||||
|
|
||||||
// For color preview
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
|
||||||
|
|
||||||
// For attention
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
|
||||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
|
||||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
|
||||||
|
|
||||||
// For Chroma markdown plugin
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
|
||||||
|
|
||||||
// Checkboxes
|
|
||||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
|
||||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
|
||||||
|
|
||||||
// Custom URL-Schemes
|
|
||||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
|
||||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
|
||||||
} else {
|
|
||||||
policy.AllowURLSchemesMatching(allowAllRegex)
|
|
||||||
|
|
||||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
|
||||||
disallowScheme := func(*url.URL) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
|
||||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
|
||||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow classes for anchors
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
|
||||||
|
|
||||||
// Allow classes for task lists
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
|
||||||
|
|
||||||
// Allow classes for org mode list item status.
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
|
||||||
|
|
||||||
// Allow icons
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
|
||||||
|
|
||||||
// Allow classes for emojis
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
|
||||||
|
|
||||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
|
||||||
|
|
||||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
|
||||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
|
||||||
|
|
||||||
// Allow generally safe attributes
|
|
||||||
generalSafeAttrs := []string{
|
|
||||||
"abbr", "accept", "accept-charset",
|
|
||||||
"accesskey", "action", "align", "alt",
|
|
||||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
|
||||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
|
||||||
"charoff", "charset", "checked",
|
|
||||||
"clear", "cols", "colspan", "color",
|
|
||||||
"compact", "coords", "datetime", "dir",
|
|
||||||
"disabled", "enctype", "for", "frame",
|
|
||||||
"headers", "height", "hreflang",
|
|
||||||
"hspace", "ismap", "label", "lang",
|
|
||||||
"maxlength", "media", "method",
|
|
||||||
"multiple", "name", "nohref", "noshade",
|
|
||||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
|
||||||
"rows", "rowspan", "rules", "scope",
|
|
||||||
"selected", "shape", "size", "span",
|
|
||||||
"start", "summary", "tabindex", "target",
|
|
||||||
"title", "type", "usemap", "valign", "value",
|
|
||||||
"vspace", "width", "itemprop",
|
|
||||||
}
|
|
||||||
|
|
||||||
generalSafeElements := []string{
|
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
|
||||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
|
||||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
|
||||||
"details", "caption", "figure", "figcaption",
|
|
||||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
|
||||||
}
|
|
||||||
|
|
||||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
|
||||||
|
|
||||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
|
||||||
|
|
||||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
|
||||||
|
|
||||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
|
||||||
|
|
||||||
// Custom keyword markup
|
|
||||||
addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
|
||||||
|
|
||||||
return policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
|
||||||
// repository descriptions.
|
|
||||||
func createRepoDescriptionPolicy() *bluemonday.Policy {
|
|
||||||
policy := bluemonday.NewPolicy()
|
|
||||||
|
|
||||||
// Allow italics and bold.
|
|
||||||
policy.AllowElements("i", "b", "em", "strong")
|
|
||||||
|
|
||||||
// Allow code.
|
|
||||||
policy.AllowElements("code")
|
|
||||||
|
|
||||||
// Allow links
|
|
||||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
|
||||||
|
|
||||||
// Allow classes for emojis
|
|
||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
|
||||||
policy.AllowAttrs("aria-label").OnElements("span")
|
|
||||||
|
|
||||||
return policy
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.AllowDataURIImages {
|
|
||||||
policy.AllowDataURIImages()
|
|
||||||
}
|
|
||||||
if rule.Element != "" {
|
|
||||||
if rule.Regexp != nil {
|
|
||||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
|
||||||
} else {
|
|
||||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
|
||||||
func SanitizeDescription(s string) string {
|
|
||||||
NewSanitizer()
|
|
||||||
return sanitizer.descriptionPolicy.Sanitize(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
|
||||||
func Sanitize(s string) string {
|
|
||||||
NewSanitizer()
|
|
||||||
return sanitizer.defaultPolicy.Sanitize(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SanitizeReader sanitizes a Reader
|
|
||||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
|
||||||
NewSanitizer()
|
|
||||||
policy, exist := sanitizer.rendererPolicies[renderer]
|
|
||||||
if !exist {
|
|
||||||
policy = sanitizer.defaultPolicy
|
|
||||||
}
|
|
||||||
return policy.SanitizeReaderToWriter(r, w)
|
|
||||||
}
|
}
|
||||||
|
|
25
modules/markup/sanitizer_custom.go
Normal file
25
modules/markup/sanitizer_custom.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||||
|
for _, rule := range rules {
|
||||||
|
if rule.AllowDataURIImages {
|
||||||
|
policy.AllowDataURIImages()
|
||||||
|
}
|
||||||
|
if rule.Element != "" {
|
||||||
|
if rule.Regexp != nil {
|
||||||
|
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||||
|
} else {
|
||||||
|
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
modules/markup/sanitizer_default.go
Normal file
146
modules/markup/sanitizer_default.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||||
|
policy := bluemonday.UGCPolicy()
|
||||||
|
|
||||||
|
// For JS code copy and Mermaid loading state
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||||
|
|
||||||
|
// For code preview
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||||
|
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||||
|
|
||||||
|
// For code preview (unicode escape)
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||||
|
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||||
|
|
||||||
|
// For color preview
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||||
|
|
||||||
|
// For attention
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||||
|
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||||
|
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||||
|
|
||||||
|
// For Chroma markdown plugin
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||||
|
|
||||||
|
// Checkboxes
|
||||||
|
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||||
|
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||||
|
|
||||||
|
// Custom URL-Schemes
|
||||||
|
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||||
|
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||||
|
} else {
|
||||||
|
policy.AllowURLSchemesMatching(st.allowAllRegex)
|
||||||
|
|
||||||
|
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||||
|
disallowScheme := func(*url.URL) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||||
|
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||||
|
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow classes for anchors
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||||
|
|
||||||
|
// Allow classes for task lists
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||||
|
|
||||||
|
// Allow classes for org mode list item status.
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||||
|
|
||||||
|
// Allow icons
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||||
|
|
||||||
|
// Allow classes for emojis
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||||
|
|
||||||
|
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||||
|
|
||||||
|
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||||
|
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||||
|
|
||||||
|
// Allow generally safe attributes
|
||||||
|
generalSafeAttrs := []string{
|
||||||
|
"abbr", "accept", "accept-charset",
|
||||||
|
"accesskey", "action", "align", "alt",
|
||||||
|
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||||
|
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||||
|
"charoff", "charset", "checked",
|
||||||
|
"clear", "cols", "colspan", "color",
|
||||||
|
"compact", "coords", "datetime", "dir",
|
||||||
|
"disabled", "enctype", "for", "frame",
|
||||||
|
"headers", "height", "hreflang",
|
||||||
|
"hspace", "ismap", "label", "lang",
|
||||||
|
"maxlength", "media", "method",
|
||||||
|
"multiple", "name", "nohref", "noshade",
|
||||||
|
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||||
|
"rows", "rowspan", "rules", "scope",
|
||||||
|
"selected", "shape", "size", "span",
|
||||||
|
"start", "summary", "tabindex", "target",
|
||||||
|
"title", "type", "usemap", "valign", "value",
|
||||||
|
"vspace", "width", "itemprop",
|
||||||
|
}
|
||||||
|
|
||||||
|
generalSafeElements := []string{
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||||
|
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||||
|
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||||
|
"details", "caption", "figure", "figcaption",
|
||||||
|
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||||
|
|
||||||
|
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||||
|
|
||||||
|
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||||
|
|
||||||
|
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||||
|
|
||||||
|
// Custom keyword markup
|
||||||
|
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||||
|
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||||
|
func Sanitize(s string) string {
|
||||||
|
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeReader sanitizes a Reader
|
||||||
|
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||||
|
policy, exist := GetDefaultSanitizer().rendererPolicies[renderer]
|
||||||
|
if !exist {
|
||||||
|
policy = GetDefaultSanitizer().defaultPolicy
|
||||||
|
}
|
||||||
|
return policy.SanitizeReaderToWriter(r, w)
|
||||||
|
}
|
|
@ -5,18 +5,16 @@
|
||||||
package markup
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Sanitizer(t *testing.T) {
|
func TestSanitizer(t *testing.T) {
|
||||||
NewSanitizer()
|
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
// Regular
|
// Regular
|
||||||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
||||||
|
"<scrİpt><script>alert(document.domain)</script></scrİpt>", "<script>alert(document.domain)</script>",
|
||||||
|
|
||||||
// Code highlighting class
|
// Code highlighting class
|
||||||
`<code class="random string"></code>`, `<code></code>`,
|
`<code class="random string"></code>`, `<code></code>`,
|
||||||
|
@ -72,34 +70,3 @@ func Test_Sanitizer(t *testing.T) {
|
||||||
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
|
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDescriptionSanitizer(t *testing.T) {
|
|
||||||
NewSanitizer()
|
|
||||||
|
|
||||||
testCases := []string{
|
|
||||||
`<h1>Title</h1>`, `Title`,
|
|
||||||
`<img src='img.png' alt='image'>`, ``,
|
|
||||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
|
||||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
|
||||||
`<br>`, ``,
|
|
||||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
|
|
||||||
`<mark>Important!</mark>`, `Important!`,
|
|
||||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
|
||||||
`<input type="hidden">`, ``,
|
|
||||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
|
||||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(testCases); i += 2 {
|
|
||||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeNonEscape(t *testing.T) {
|
|
||||||
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
|
||||||
|
|
||||||
output := template.HTML(Sanitize(descStr))
|
|
||||||
if strings.Contains(string(output), "<script>") {
|
|
||||||
t.Errorf("un-escaped <script> in output: %q", output)
|
|
||||||
}
|
|
||||||
}
|
|
37
modules/markup/sanitizer_description.go
Normal file
37
modules/markup/sanitizer_description.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||||
|
// repository descriptions.
|
||||||
|
func (st *Sanitizer) createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||||
|
policy := bluemonday.NewPolicy()
|
||||||
|
policy.AllowStandardURLs()
|
||||||
|
|
||||||
|
// Allow italics and bold.
|
||||||
|
policy.AllowElements("i", "b", "em", "strong")
|
||||||
|
|
||||||
|
// Allow code.
|
||||||
|
policy.AllowElements("code")
|
||||||
|
|
||||||
|
// Allow links
|
||||||
|
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||||
|
|
||||||
|
// Allow classes for emojis
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||||
|
policy.AllowAttrs("aria-label").OnElements("span")
|
||||||
|
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||||
|
func SanitizeDescription(s string) string {
|
||||||
|
return GetDefaultSanitizer().descriptionPolicy.Sanitize(s)
|
||||||
|
}
|
31
modules/markup/sanitizer_description_test.go
Normal file
31
modules/markup/sanitizer_description_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDescriptionSanitizer(t *testing.T) {
|
||||||
|
testCases := []string{
|
||||||
|
`<h1>Title</h1>`, `Title`,
|
||||||
|
`<img src='img.png' alt='image'>`, ``,
|
||||||
|
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||||
|
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||||
|
`<br>`, ``,
|
||||||
|
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">https://example.com</a>`,
|
||||||
|
`<a href="data:1234">data</a>`, `data`,
|
||||||
|
`<mark>Important!</mark>`, `Important!`,
|
||||||
|
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||||
|
`<input type="hidden">`, ``,
|
||||||
|
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||||
|
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(testCases); i += 2 {
|
||||||
|
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue