File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/markup/sanitize_test.go
168 lines · 7.0kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package markup
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"strings"
133728e barerepo 1mo
5
"testing"
133728e barerepo 1mo
6
)
133728e barerepo 1mo
7
133728e barerepo 1mo
8
// Chapter 45.4's attacks, each a permanent test: no tag survives, though inert words may.
133728e barerepo 1mo
9
func TestChapter45Attacks(t *testing.T) {
133728e barerepo 1mo
10
cases := []struct {
133728e barerepo 1mo
11
name, body string
133728e barerepo 1mo
12
forbidden []string
133728e barerepo 1mo
13
}{
133728e barerepo 1mo
14
{"script tag", `<script>alert(1)</script>`, []string{"<script"}},
133728e barerepo 1mo
15
{"script inline in a paragraph", `hello <script>alert(1)</script> there`, []string{"<script"}},
133728e barerepo 1mo
16
{"script split across lines", "a\n<script>\nalert(1)\n</script>\nb", []string{"<script"}},
133728e barerepo 1mo
17
{"javascript link", `[x](javascript:alert(1))`, []string{"javascript:"}},
133728e barerepo 1mo
18
{"image onerror", `<img src=x onerror=alert(1)>`, []string{"onerror", "alert(1)"}},
133728e barerepo 1mo
19
{"encoded javascript", `<a href="java&#115;cript:alert(1)">x</a>`, []string{"javascript:", "java&#115;cript"}},
133728e barerepo 1mo
20
{"data url", `[x](data:text/html;base64,PHNjcmlwdD4=)`, []string{"data:"}},
133728e barerepo 1mo
21
{"vbscript", `[x](vbscript:msgbox(1))`, []string{"vbscript:"}},
133728e barerepo 1mo
22
{"file url", `[x](file:///etc/passwd)`, []string{"file:"}},
133728e barerepo 1mo
23
{"iframe", `<iframe src="https://evil.example"></iframe>`, []string{"<iframe", "evil.example"}},
133728e barerepo 1mo
24
{"form", `<form action="/x"><input name="p"></form>`, []string{"<form", "<input"}},
133728e barerepo 1mo
25
{"svg", `<svg onload="alert(1)"><circle /></svg>`, []string{"<svg", "onload"}},
133728e barerepo 1mo
26
{"style attribute", `<p style="position:fixed;top:0">x</p>`, []string{"style="}},
133728e barerepo 1mo
27
{"style element", `<style>body{display:none}</style>`, []string{"<style", "display:none"}},
133728e barerepo 1mo
28
{"object", `<object data="evil.swf"></object>`, []string{"<object"}},
133728e barerepo 1mo
29
{"embed", `<embed src="evil.swf">`, []string{"<embed"}},
133728e barerepo 1mo
30
{"leading space scheme", `<a href=" javascript:alert(1)">x</a>`, []string{"javascript:"}},
133728e barerepo 1mo
31
{"upper case scheme", `<a href="JaVaScRiPt:alert(1)">x</a>`, []string{"avascript:", "alert(1)"}},
133728e barerepo 1mo
32
{"null byte scheme", "<a href=\"java\x00script:alert(1)\">x</a>", []string{"script:alert"}},
133728e barerepo 1mo
33
{"meta refresh", `<meta http-equiv="refresh" content="0;url=https://evil.example">`, []string{"<meta", "refresh"}},
133728e barerepo 1mo
34
{"base tag", `<base href="https://evil.example/">`, []string{"<base"}},
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
for _, c := range cases {
133728e barerepo 1mo
37
got := strings.ToLower(Render(c.body))
133728e barerepo 1mo
38
for _, bad := range c.forbidden {
133728e barerepo 1mo
39
if strings.Contains(got, strings.ToLower(bad)) {
133728e barerepo 1mo
40
t.Errorf("%s: output still contains %q\n %s", c.name, bad, got)
133728e barerepo 1mo
41
}
133728e barerepo 1mo
42
}
133728e barerepo 1mo
43
}
133728e barerepo 1mo
44
}
133728e barerepo 1mo
45
133728e barerepo 1mo
46
// What survives matters too, because a sanitizer that strips everything is safe and useless.
133728e barerepo 1mo
47
func TestOrdinaryMarkdownSurvives(t *testing.T) {
133728e barerepo 1mo
48
body := "# heading\n\nsome **bold** and *italic* and `code`.\n\n" +
133728e barerepo 1mo
49
"- one\n- two\n\n> quoted\n\n```go\nfunc main() {}\n```\n\n" +
133728e barerepo 1mo
50
"[forge](https://barerepo.example) and a table:\n\n" +
133728e barerepo 1mo
51
"| a | b |\n|---|---|\n| 1 | 2 |\n"
133728e barerepo 1mo
52
got := Render(body)
133728e barerepo 1mo
53
for _, want := range []string{
133728e barerepo 1mo
54
"<h1>heading</h1>", "<strong>bold</strong>", "<em>italic</em>",
133728e barerepo 1mo
55
"<code>code</code>", "<ul>", "<li>one</li>", "<blockquote>",
133728e barerepo 1mo
56
"<pre>", "func main()", "<table>", "<td>1</td>",
133728e barerepo 1mo
57
`<a href="https://barerepo.example"`,
133728e barerepo 1mo
58
} {
133728e barerepo 1mo
59
if !strings.Contains(got, want) {
133728e barerepo 1mo
60
t.Errorf("ordinary markdown lost %q\n %s", want, got)
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
}
133728e barerepo 1mo
63
}
133728e barerepo 1mo
64
133728e barerepo 1mo
65
// Chapter 42.2: mark every external link, so no comment passes a referrer or window handle.
133728e barerepo 1mo
66
func TestExternalLinksAreMarked(t *testing.T) {
133728e barerepo 1mo
67
got := Render(`[x](https://elsewhere.example)`)
133728e barerepo 1mo
68
if !strings.Contains(got, `rel="nofollow noopener noreferrer"`) {
133728e barerepo 1mo
69
t.Errorf("an external link has no rel attribute: %s", got)
133728e barerepo 1mo
70
}
133728e barerepo 1mo
71
local := Render(`[x](/john/johnbot)`)
133728e barerepo 1mo
72
if strings.Contains(local, "nofollow") {
133728e barerepo 1mo
73
t.Errorf("a link inside the site was marked external: %s", local)
133728e barerepo 1mo
74
}
133728e barerepo 1mo
75
}
133728e barerepo 1mo
76
133728e barerepo 1mo
77
// Chapter 42.2: a remote image leaks the reader's address, and blocking is simpler and honest.
133728e barerepo 1mo
78
func TestRemoteImagesAreBlocked(t *testing.T) {
133728e barerepo 1mo
79
got := Render(`![x](https://tracker.example/pixel.gif)`)
133728e barerepo 1mo
80
if strings.Contains(got, "tracker.example") {
133728e barerepo 1mo
81
t.Errorf("a remote image survived: %s", got)
133728e barerepo 1mo
82
}
133728e barerepo 1mo
83
if !strings.Contains(got, "<img") {
133728e barerepo 1mo
84
return // dropping the whole tag is also an acceptable answer
133728e barerepo 1mo
85
}
133728e barerepo 1mo
86
}
133728e barerepo 1mo
87
133728e barerepo 1mo
88
// A language class on a code block is the one class allowed, in chapter 42.1's form only.
133728e barerepo 1mo
89
func TestCodeClass(t *testing.T) {
133728e barerepo 1mo
90
if got := Sanitize(`<code class="language-go">x</code>`); !strings.Contains(got, `class="language-go"`) {
133728e barerepo 1mo
91
t.Errorf("a language class was stripped: %s", got)
133728e barerepo 1mo
92
}
133728e barerepo 1mo
93
if got := Sanitize(`<code class="anything-else">x</code>`); strings.Contains(got, "class") {
133728e barerepo 1mo
94
t.Errorf("an arbitrary class survived: %s", got)
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
133728e barerepo 1mo
98
// Text inside a stripped element stays out, or a script's body lands in front of the reader.
133728e barerepo 1mo
99
func TestDroppedElementsTakeTheirContents(t *testing.T) {
133728e barerepo 1mo
100
got := Sanitize(`<script>var secret = 1;</script><p>kept</p>`)
133728e barerepo 1mo
101
if strings.Contains(got, "secret") {
133728e barerepo 1mo
102
t.Errorf("the body of a script survived: %s", got)
133728e barerepo 1mo
103
}
133728e barerepo 1mo
104
if !strings.Contains(got, "kept") {
133728e barerepo 1mo
105
t.Errorf("the text after it was lost: %s", got)
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
133728e barerepo 1mo
109
// A tag never opened must not close, or a comment escapes its box into the page's layout.
133728e barerepo 1mo
110
func TestUnbalancedTagsCannotEscape(t *testing.T) {
133728e barerepo 1mo
111
got := Sanitize(`</div></body><p>hi</p>`)
133728e barerepo 1mo
112
for _, bad := range []string{"</div>", "</body>"} {
133728e barerepo 1mo
113
if strings.Contains(got, bad) {
133728e barerepo 1mo
114
t.Errorf("a stray %q survived: %s", bad, got)
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
}
133728e barerepo 1mo
117
open := strings.Count(Sanitize("<p>a<p>b"), "<p>")
133728e barerepo 1mo
118
closed := strings.Count(Sanitize("<p>a<p>b"), "</p>")
133728e barerepo 1mo
119
if open != closed {
133728e barerepo 1mo
120
t.Errorf("unbalanced output: %d open, %d closed", open, closed)
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
133728e barerepo 1mo
124
// Prose that mentions a tag keeps its sentence: the tag goes, the words stay, backticks show markup.
133728e barerepo 1mo
125
func TestProseAroundTagsSurvives(t *testing.T) {
133728e barerepo 1mo
126
got := Render("use <div> for that")
133728e barerepo 1mo
127
if !strings.Contains(got, "use") || !strings.Contains(got, "for that") {
133728e barerepo 1mo
128
t.Errorf("the sentence was lost: %s", got)
133728e barerepo 1mo
129
}
133728e barerepo 1mo
130
// Backticks are the way to show a tag, and they work.
133728e barerepo 1mo
131
code := Render("use `<div>` for that")
133728e barerepo 1mo
132
if !strings.Contains(code, "&lt;div&gt;") {
133728e barerepo 1mo
133
t.Errorf("a tag in backticks did not survive: %s", code)
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
133728e barerepo 1mo
137
// Chapter 42.2 blocks a remote image and says so, because a silent gap reads as a broken page.
133728e barerepo 1mo
138
func TestARemoteImageIsBlockedOutLoud(t *testing.T) {
133728e barerepo 1mo
139
got := Render("![a cat](https://example.com/cat.png)")
133728e barerepo 1mo
140
if strings.Contains(got, "example.com") {
133728e barerepo 1mo
141
t.Errorf("a remote image reached the page, so the reader's address goes with it:\n%s", got)
133728e barerepo 1mo
142
}
133728e barerepo 1mo
143
if !strings.Contains(got, "remote image blocked") {
133728e barerepo 1mo
144
t.Errorf("the page does not say the image was blocked:\n%s", got)
133728e barerepo 1mo
145
}
133728e barerepo 1mo
146
if strings.Contains(got, "<img") {
133728e barerepo 1mo
147
t.Errorf("a source-less img was left behind, which draws as broken:\n%s", got)
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
133728e barerepo 1mo
150
// A local image is barerepo's own bytes and stays.
133728e barerepo 1mo
151
local := Render("![a diagram](/john/johnbot/raw/master/doc.png)")
133728e barerepo 1mo
152
if !strings.Contains(local, "<img") || strings.Contains(local, "remote image blocked") {
133728e barerepo 1mo
153
t.Errorf("a local image was blocked:\n%s", local)
133728e barerepo 1mo
154
}
133728e barerepo 1mo
155
}
133728e barerepo 1mo
156
133728e barerepo 1mo
157
func TestOrderedListKeepsItsStart(t *testing.T) {
133728e barerepo 1mo
158
got := Render("6. six\n7. seven\n")
133728e barerepo 1mo
159
if !strings.Contains(got, `start="6"`) {
133728e barerepo 1mo
160
t.Fatalf("start was dropped: %s", got)
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
if bad := Render("1. one\n"); strings.Contains(bad, "start=") {
133728e barerepo 1mo
163
t.Fatalf("a list starting at one should not carry start: %s", bad)
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
if evil := Render(`<ol start="x() javascript:"><li>a</li></ol>`); strings.Contains(evil, "start=") {
133728e barerepo 1mo
166
t.Fatalf("a non numeric start should be dropped: %s", evil)
133728e barerepo 1mo
167
}
133728e barerepo 1mo
168
}
history · rawbarerepo 0.1.0