mirror of
https://github.com/go-gitea/gitea
synced 2024-12-18 09:45:47 +01:00
Split sanitizer functions and fine-tune some tests (#31192)
This commit is contained in:
parent
352a2cae24
commit
a4275951ba
8 changed files with 270 additions and 245 deletions
|
@ -169,13 +169,18 @@ func TestRender_links(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
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"}
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
|
||||
// Text that should be turned into URL
|
||||
test(
|
||||
"https://www.example.com",
|
||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
||||
|
@ -259,11 +264,6 @@ func TestRender_links(t *testing.T) {
|
|||
test(
|
||||
"ftps://gitea.com",
|
||||
`<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) {
|
||||
|
|
|
@ -47,7 +47,6 @@ func Init(ph *ProcessorHelper) {
|
|||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
NewSanitizer()
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
|
|
@ -5,13 +5,9 @@
|
|||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
|
@ -21,211 +17,35 @@ type Sanitizer struct {
|
|||
defaultPolicy *bluemonday.Policy
|
||||
descriptionPolicy *bluemonday.Policy
|
||||
rendererPolicies map[string]*bluemonday.Policy
|
||||
init sync.Once
|
||||
allowAllRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
sanitizer = &Sanitizer{}
|
||||
allowAllRegex = regexp.MustCompile(".+")
|
||||
defaultSanitizer *Sanitizer
|
||||
defaultSanitizerOnce sync.Once
|
||||
)
|
||||
|
||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
||||
// entire application lifecycle.
|
||||
func NewSanitizer() {
|
||||
sanitizer.init.Do(func() {
|
||||
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 {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := createDefaultPolicy()
|
||||
addSanitizerRules(policy, sanitizerRules)
|
||||
sanitizer.rendererPolicies[name] = policy
|
||||
func GetDefaultSanitizer() *Sanitizer {
|
||||
defaultSanitizerOnce.Do(func() {
|
||||
defaultSanitizer = &Sanitizer{
|
||||
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||
allowAllRegex: regexp.MustCompile(".+"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func 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(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)
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||
defaultSanitizer.rendererPolicies[name] = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||
})
|
||||
return defaultSanitizer
|
||||
}
|
||||
|
||||
// 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)
|
||||
func ResetDefaultSanitizerForTesting() {
|
||||
defaultSanitizer = nil
|
||||
defaultSanitizerOnce = sync.Once{}
|
||||
}
|
||||
|
|
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
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Sanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
func TestSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
// Regular
|
||||
`<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 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]))
|
||||
}
|
||||
}
|
||||
|
||||
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