oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "errors"
5 "strings"
6 "testing"
7)
8
9// TestSvgIsSanitised is the file-level half of the brief's test of the
10// same name (the endpoint's half is in internal/slidestest): what
11// survives an upload, what is quietly removed from it, and what makes
12// the whole document a refusal.
13func TestSvgIsSanitised(t *testing.T) {
14 t.Run("a plain drawing survives, byte for byte where it can", func(t *testing.T) {
15 in := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">` +
16 `<!-- a comment --><title>A red square</title>` +
17 `<rect x="1" y="1" width="8" height="8" fill="#c00"/>` +
18 `<use href="#other"/></svg>`
19 out, err := sanitiseSVG([]byte(in))
20 if err != nil {
21 t.Fatalf("sanitiseSVG: %v", err)
22 }
23 for _, want := range []string{`viewBox="0 0 10 10"`, `<title>A red square</title>`,
24 `fill="#c00"`, `<use href="#other"/>`, `<!-- a comment -->`} {
25 if !strings.Contains(string(out), want) {
26 t.Errorf("the drawing lost %s:\n%s", want, out)
27 }
28 }
29 })
30
31 t.Run("script, foreign HTML and event handlers are stripped", func(t *testing.T) {
32 in := `<svg xmlns="http://www.w3.org/2000/svg" onload="steal()">` +
33 `<script>fetch('/steal')</script>` +
34 `<foreignObject width="10" height="10"><body xmlns="http://www.w3.org/1999/xhtml">` +
35 `<script>more()</script>hi</body></foreignObject>` +
36 `<rect onclick="alert(1)" ONMOUSEOVER="alert(2)" fill="#000"/>` +
37 `<a href="javascript:alert(3)"><text>go</text></a>` +
38 `<a href="java&#115;cript:alert(4)"><text>sneaky</text></a>` +
39 `</svg>`
40 out, err := sanitiseSVG([]byte(in))
41 if err != nil {
42 t.Fatalf("sanitiseSVG: %v", err)
43 }
44 s := strings.ToLower(string(out))
45 for _, gone := range []string{"script", "foreignobject", "onload", "onclick",
46 "onmouseover", "javascript", "steal", "alert", "hi"} {
47 if strings.Contains(s, gone) {
48 t.Errorf("%q survived:\n%s", gone, out)
49 }
50 }
51 // What was around the removed parts is still a drawing.
52 if !strings.Contains(string(out), `fill="#000"`) {
53 t.Errorf("the rect lost its fill:\n%s", out)
54 }
55 if !strings.Contains(string(out), "<text>go</text>") {
56 t.Errorf("the link's text was dropped with its href:\n%s", out)
57 }
58 })
59
60 t.Run("an external reference is refused, not patched", func(t *testing.T) {
61 for name, in := range map[string]string{
62 "an image from another site": `<svg xmlns="http://www.w3.org/2000/svg"><image href="https://example.test/x.png"/></svg>`,
63 "a protocol-relative use": `<svg xmlns="http://www.w3.org/2000/svg"><use xlink:href="//example.test/s.svg#a"/></svg>`,
64 "an @import in a style": `<svg xmlns="http://www.w3.org/2000/svg"><style>@import url("https://example.test/x.css");</style></svg>`,
65 "a url() in a style attr": `<svg xmlns="http://www.w3.org/2000/svg"><rect style="fill:url(https://example.test/x.png)"/></svg>`,
66 "an external svg stylesheet": `<svg xmlns="http://www.w3.org/2000/svg"><style>rect{fill:url("http://example.test/p.png")}</style></svg>`,
67 } {
68 if _, err := sanitiseSVG([]byte(in)); !errors.Is(err, errSVGExternal) {
69 t.Errorf("%s: want errSVGExternal, got %v", name, err)
70 }
71 }
72 })
73
74 t.Run("a document that is not a safe SVG at all is refused", func(t *testing.T) {
75 for name, in := range map[string]string{
76 "a doctype": `<!DOCTYPE svg [<!ENTITY x "boom">]><svg xmlns="http://www.w3.org/2000/svg">&x;</svg>`,
77 "not XML": `<svg><rect fill=red></svg>`,
78 "another root": `<html><body>hi</body></html>`,
79 "nothing at all": ``,
80 "a proc inst": `<?xml-stylesheet href="x.css"?><svg xmlns="http://www.w3.org/2000/svg"/>`,
81 } {
82 if _, err := sanitiseSVG([]byte(in)); !errors.Is(err, errNotSVG) {
83 t.Errorf("%s: want errNotSVG, got %v", name, err)
84 }
85 }
86 })
87
88 t.Run("an inline raster stays, an inline SVG does not", func(t *testing.T) {
89 raster := `<svg xmlns="http://www.w3.org/2000/svg"><image href="data:image/png;base64,iVBOR"/></svg>`
90 out, err := sanitiseSVG([]byte(raster))
91 if err != nil {
92 t.Fatalf("an inline PNG should be kept: %v", err)
93 }
94 if !strings.Contains(string(out), "data:image/png") {
95 t.Errorf("the inline PNG was dropped:\n%s", out)
96 }
97 // A nested SVG through a data: URL is a second document with a
98 // second set of everything this file strips, and there is no
99 // second pass — so it is refused rather than silently emptied.
100 nested := `<svg xmlns="http://www.w3.org/2000/svg"><image href="data:image/svg+xml;base64,PHN2Zz4="/></svg>`
101 if _, err := sanitiseSVG([]byte(nested)); !errors.Is(err, errSVGExternal) {
102 t.Errorf("a nested SVG: want errSVGExternal, got %v", err)
103 }
104 })
105}
106