Skip to content

Commit 9025a69

Browse files
committed
Support custom sanitization policy
Allowing the gitea administrator to configure sanitization policy allows them to couple external renders and custom templates to support more markup. In particular, the `pandoc` renderer allows generating KaTeX annotations, wrapping them in `<span>` elements with class `math` and either `inline` or `display` (depending on whether or not inline or block mode was requested). This iteration gives the administrator whitelisting powers; carefully crafted regexes will thus let through only the desired attributes necessary to support their custom markup. Resolves: go-gitea#9054 Signed-off-by: Alexander Scheel <[email protected]>
1 parent d672206 commit 9025a69

File tree

2 files changed

+107
-22
lines changed

2 files changed

+107
-22
lines changed

modules/markup/sanitizer.go

+9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ func ReplaceSanitizer() {
5050

5151
// Allow <kbd> tags for keyboard shortcut styling
5252
sanitizer.policy.AllowElements("kbd")
53+
54+
// Custom keyword markup
55+
for _, rule := range setting.ExternalSanitizerRules {
56+
if rule.Regexp != nil {
57+
sanitizer.policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
58+
} else {
59+
sanitizer.policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
60+
}
61+
}
5362
}
5463

5564
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.

modules/setting/markup.go

+98-22
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import (
99
"strings"
1010

1111
"code.gitea.io/gitea/modules/log"
12+
13+
"gopkg.in/ini.v1"
1214
)
1315

1416
// ExternalMarkupParsers represents the external markup parsers
1517
var (
16-
ExternalMarkupParsers []MarkupParser
18+
ExternalMarkupParsers []MarkupParser
19+
ExternalSanitizerRules []MarkupSanitizerRule
1720
)
1821

1922
// MarkupParser defines the external parser configured in ini
@@ -25,42 +28,115 @@ type MarkupParser struct {
2528
IsInputFile bool
2629
}
2730

31+
// MarkupSanitizerRule defines the policy for whitelisting attributes on
32+
// certain elements.
33+
type MarkupSanitizerRule struct {
34+
Element string
35+
AllowAttr string
36+
Regexp *regexp.Regexp
37+
}
38+
2839
func newMarkup() {
29-
extensionReg := regexp.MustCompile(`\.\w`)
3040
for _, sec := range Cfg.Section("markup").ChildSections() {
3141
name := strings.TrimPrefix(sec.Name(), "markup.")
3242
if name == "" {
3343
log.Warn("name is empty, markup " + sec.Name() + "ignored")
3444
continue
3545
}
3646

37-
extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
38-
var exts = make([]string, 0, len(extensions))
39-
for _, extension := range extensions {
40-
if !extensionReg.MatchString(extension) {
41-
log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
42-
} else {
43-
exts = append(exts, extension)
44-
}
47+
if name == "sanitizer" {
48+
newMarkupSanitizer(name, sec)
49+
} else {
50+
newMarkupRenderer(name, sec)
4551
}
52+
}
53+
}
54+
55+
func newMarkupSanitizer(name string, sec *ini.Section) {
56+
haveElement := sec.HasKey("ELEMENT")
57+
haveAttr := sec.HasKey("ALLOW_ATTR")
58+
haveRegexp := sec.HasKey("REGEXP")
59+
60+
if !haveElement && !haveAttr && !haveRegexp {
61+
log.Warn("Skipping empty section: markup.%s.", name)
62+
return
63+
}
64+
65+
if !haveElement || !haveAttr || !haveRegexp {
66+
log.Error("Missing required keys from markup.%s. Must have all three of ELEMENT, ALLOW_ATTR, and REGEXP defined!", name)
67+
return
68+
}
69+
70+
elements := sec.Key("ELEMENT").ValueWithShadows()
71+
allowAttrs := sec.Key("ALLOW_ATTR").ValueWithShadows()
72+
regexps := sec.Key("REGEXP").ValueWithShadows()
73+
74+
if len(elements) != len(allowAttrs) ||
75+
len(elements) != len(regexps) ||
76+
len(allowAttrs) != len(regexps) {
77+
log.Error("All three keys in markup.%s (ELEMENT, ALLOW_ATTR, REGEXP) must be defined the same number of times! Got %d, %d, and %d respectively.", name, len(elements), len(allowAttrs), len(regexps))
78+
return
79+
}
4680

47-
if len(exts) == 0 {
48-
log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
81+
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, len(elements))
82+
83+
for index, pattern := range regexps {
84+
if pattern == "" {
85+
rule := MarkupSanitizerRule{
86+
Element: elements[index],
87+
AllowAttr: allowAttrs[index],
88+
Regexp: nil,
89+
}
90+
ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
4991
continue
5092
}
5193

52-
command := sec.Key("RENDER_COMMAND").MustString("")
53-
if command == "" {
54-
log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
94+
// Validate when parsing the config that this is a valid regular
95+
// expression. Then we can use regexp.MustCompile(...) later.
96+
compiled, err := regexp.Compile(pattern)
97+
if err != nil {
98+
log.Error("In module.%s: REGEXP at definition %d failed to compile: %v", name, index+1, err)
5599
continue
56100
}
57101

58-
ExternalMarkupParsers = append(ExternalMarkupParsers, MarkupParser{
59-
Enabled: sec.Key("ENABLED").MustBool(false),
60-
MarkupName: name,
61-
FileExtensions: exts,
62-
Command: command,
63-
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
64-
})
102+
rule := MarkupSanitizerRule{
103+
Element: elements[index],
104+
AllowAttr: allowAttrs[index],
105+
Regexp: compiled,
106+
}
107+
ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
108+
}
109+
}
110+
111+
func newMarkupRenderer(name string, sec *ini.Section) {
112+
extensionReg := regexp.MustCompile(`\.\w`)
113+
114+
extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
115+
var exts = make([]string, 0, len(extensions))
116+
for _, extension := range extensions {
117+
if !extensionReg.MatchString(extension) {
118+
log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
119+
} else {
120+
exts = append(exts, extension)
121+
}
122+
}
123+
124+
if len(exts) == 0 {
125+
log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
126+
return
65127
}
128+
129+
command := sec.Key("RENDER_COMMAND").MustString("")
130+
if command == "" {
131+
log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
132+
return
133+
}
134+
135+
ExternalMarkupParsers = append(ExternalMarkupParsers, MarkupParser{
136+
Enabled: sec.Key("ENABLED").MustBool(false),
137+
MarkupName: name,
138+
FileExtensions: exts,
139+
Command: command,
140+
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
141+
})
66142
}

0 commit comments

Comments
 (0)