svg.gno
3.02 Kb · 114 lines
1package safe
2
3import (
4 "errors"
5 "strconv"
6 "strings"
7 "unicode/utf8"
8)
9
10// forbiddenSVG lists constructs that can run code, load something from the
11// network or redefine the document. Engines draw with shapes and paths;
12// none of these is needed for that (RA M-7).
13var forbiddenSVG = []string{
14 "<script", "javascript:", "vbscript:", "<foreignobject", "<iframe",
15 "<object", "<embed", "<image", "<!entity", "<!doctype", "<?xml-stylesheet",
16 "@import", "&#", // numeric character references would hide a scheme from this scan
17}
18
19// AssertSVG checks an engine's raw SVG output: size, envelope, the forbidden
20// constructs above, inline event handlers, and any href or url() that does
21// not point inside the document. The frontend shows art only through <img>,
22// which never runs scripts; this check is the defence in depth for every
23// other place a data: URI ends up (RA M-7, design §5.4).
24func AssertSVG(svg string, maxBytes int) error {
25 if len(svg) == 0 {
26 return errors.New("svg: empty")
27 }
28 if len(svg) > maxBytes {
29 return errors.New("svg: larger than " + strconv.Itoa(maxBytes) + " bytes")
30 }
31 if !utf8.ValidString(svg) {
32 return errors.New("svg: invalid UTF-8")
33 }
34 if !strings.HasPrefix(svg, "<svg") || !strings.HasSuffix(svg, "</svg>") {
35 return errors.New("svg: must start with <svg and end with </svg>")
36 }
37 low := strings.ToLower(svg)
38 for _, bad := range forbiddenSVG {
39 if strings.Contains(low, bad) {
40 return errors.New("svg: forbidden construct " + strconv.Quote(bad))
41 }
42 }
43 if hasEventHandler(low) {
44 return errors.New("svg: inline event handlers are not allowed")
45 }
46 if hasExternalRef(low, "href") || hasExternalRef(low, "url(") {
47 return errors.New("svg: references must stay inside the document (#id)")
48 }
49 return nil
50}
51
52// hasEventHandler finds an attribute named on<letters> (onload=, onclick =).
53func hasEventHandler(low string) bool {
54 for i := 0; i+2 < len(low); i++ {
55 if low[i] != 'o' || low[i+1] != 'n' {
56 continue
57 }
58 if i > 0 && !isAttrBoundary(low[i-1]) {
59 continue
60 }
61 j := i + 2
62 for j < len(low) && low[j] >= 'a' && low[j] <= 'z' {
63 j++
64 }
65 if j == i+2 {
66 continue
67 }
68 for j < len(low) && isSpace(low[j]) {
69 j++
70 }
71 if j < len(low) && low[j] == '=' {
72 return true
73 }
74 }
75 return false
76}
77
78// hasExternalRef reports an href attribute or url( whose target does not
79// start with '#'. key is "href" (also matches xlink:href) or "url(".
80func hasExternalRef(low, key string) bool {
81 rest := low
82 for {
83 i := strings.Index(rest, key)
84 if i < 0 {
85 return false
86 }
87 j := i + len(key)
88 if key == "href" {
89 for j < len(rest) && isSpace(rest[j]) {
90 j++
91 }
92 if j >= len(rest) || rest[j] != '=' {
93 rest = rest[i+len(key):]
94 continue
95 }
96 j++
97 }
98 for j < len(rest) && (isSpace(rest[j]) || rest[j] == '"' || rest[j] == '\'') {
99 j++
100 }
101 if j >= len(rest) || rest[j] != '#' {
102 return true
103 }
104 rest = rest[j:]
105 }
106}
107
108func isAttrBoundary(c byte) bool {
109 return isSpace(c) || c == '"' || c == '\'' || c == '/'
110}
111
112func isSpace(c byte) bool {
113 return c == ' ' || c == '\t' || c == '\n' || c == '\r'
114}