Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

svg.gno

11.02 Kb · 367 lines
  1package safe
  2
  3import (
  4	"errors"
  5	"strconv"
  6	"strings"
  7	"unicode/utf8"
  8)
  9
 10// AssertSVG checks an engine's raw SVG output with a strict allowlist
 11// (SA-M3; it replaced a substring blacklist that namespace prefixes, URL
 12// whitespace and CSS escapes could slip past). The document is tokenized
 13// as XML and must be well formed:
 14//
 15//   - only the elements in svgElements, without a namespace prefix: no
 16//     script, style, foreignObject, image, a, animation (set, animate*) or
 17//     anything else that can run code, load a resource or redefine the page;
 18//   - only the attributes in svgAttributes, without a prefix except
 19//     xlink:href and the two fixed xmlns declarations; event handlers and
 20//     style= are therefore impossible;
 21//   - attribute values hold no control character, backslash, '<', ':' or
 22//     non-allowlisted entity, so no URL scheme (javascript:, data:, http:)
 23//     can be spelled, however it is split or escaped; url() must be
 24//     url(#id) and href must be #id;
 25//   - no comments, CDATA, doctype, entities or processing instructions.
 26//
 27// The frontend shows art only through <img>, which never runs scripts; this
 28// check is the defence in depth for every other place a data: URI ends up
 29// (wallets, explorers, indexers; RA M-7, design §5.4).
 30func AssertSVG(svg string, maxBytes int) error {
 31	if len(svg) == 0 {
 32		return errors.New("svg: empty")
 33	}
 34	if len(svg) > maxBytes {
 35		return errors.New("svg: larger than " + strconv.Itoa(maxBytes) + " bytes")
 36	}
 37	if !utf8.ValidString(svg) {
 38		return errors.New("svg: invalid UTF-8")
 39	}
 40	if !strings.HasPrefix(svg, "<svg") || !strings.HasSuffix(svg, "</svg>") {
 41		return errors.New("svg: must start with <svg and end with </svg>")
 42	}
 43	p := &svgParser{s: svg}
 44	return p.run()
 45}
 46
 47const (
 48	svgNS   = "http://www.w3.org/2000/svg"
 49	xlinkNS = "http://www.w3.org/1999/xlink"
 50)
 51
 52// svgElements are the drawing elements an engine may use.
 53var svgElements = map[string]bool{
 54	"svg": true, "g": true, "defs": true, "symbol": true, "use": true,
 55	"title": true, "desc": true,
 56	"path": true, "rect": true, "circle": true, "ellipse": true,
 57	"line": true, "polyline": true, "polygon": true,
 58	"text": true, "tspan": true,
 59	"linearGradient": true, "radialGradient": true, "stop": true,
 60	"clipPath": true, "mask": true, "pattern": true,
 61	"filter": true, "feGaussianBlur": true, "feOffset": true, "feBlend": true,
 62	"feColorMatrix": true, "feComposite": true, "feFlood": true, "feMerge": true,
 63	"feMergeNode": true, "feMorphology": true, "feTurbulence": true,
 64	"feDisplacementMap": true, "feComponentTransfer": true, "feFuncR": true,
 65	"feFuncG": true, "feFuncB": true, "feFuncA": true,
 66}
 67
 68// svgAttributes are geometry, paint and filter attributes. Absent on
 69// purpose: style, on*, src, every animation attribute (attributeName, to,
 70// from, by, begin), and every prefixed name except xlink:href.
 71var svgAttributes = map[string]bool{
 72	"id": true, "class": true, "version": true, "viewBox": true, "preserveAspectRatio": true,
 73	"x": true, "y": true, "x1": true, "y1": true, "x2": true, "y2": true,
 74	"cx": true, "cy": true, "r": true, "rx": true, "ry": true, "fx": true, "fy": true, "fr": true,
 75	"width": true, "height": true, "d": true, "points": true, "pathLength": true,
 76	"transform": true, "href": true, "xlink:href": true,
 77	"fill": true, "fill-opacity": true, "fill-rule": true,
 78	"stroke": true, "stroke-width": true, "stroke-opacity": true, "stroke-linecap": true,
 79	"stroke-linejoin": true, "stroke-dasharray": true, "stroke-dashoffset": true, "stroke-miterlimit": true,
 80	"opacity": true, "color": true, "visibility": true, "display": true,
 81	"clip-path": true, "clip-rule": true, "mask": true, "filter": true,
 82	"shape-rendering": true, "vector-effect": true, "paint-order": true, "mix-blend-mode": true,
 83	"font-family": true, "font-size": true, "font-weight": true, "font-style": true,
 84	"text-anchor": true, "dominant-baseline": true, "letter-spacing": true,
 85	"textLength": true, "lengthAdjust": true, "dx": true, "dy": true, "rotate": true,
 86	"gradientUnits": true, "gradientTransform": true, "spreadMethod": true,
 87	"offset": true, "stop-color": true, "stop-opacity": true,
 88	"patternUnits": true, "patternContentUnits": true, "patternTransform": true,
 89	"clipPathUnits": true, "maskUnits": true, "maskContentUnits": true,
 90	"filterUnits": true, "primitiveUnits": true, "result": true, "in": true, "in2": true,
 91	"stdDeviation": true, "mode": true, "type": true, "values": true, "operator": true,
 92	"k1": true, "k2": true, "k3": true, "k4": true, "radius": true,
 93	"flood-color": true, "flood-opacity": true, "baseFrequency": true, "numOctaves": true,
 94	"seed": true, "stitchTiles": true, "scale": true, "xChannelSelector": true, "yChannelSelector": true,
 95	"tableValues": true, "slope": true, "intercept": true, "amplitude": true, "exponent": true,
 96	"xmlns": true, "xmlns:xlink": true,
 97}
 98
 99type svgParser struct {
100	s     string
101	i     int
102	stack []string
103}
104
105func svgErr(msg string) error { return errors.New("svg: " + msg) }
106
107func (p *svgParser) run() error {
108	rootDone := false
109	for p.i < len(p.s) {
110		if rootDone {
111			return svgErr("content after the root element")
112		}
113		if p.s[p.i] != '<' {
114			if err := p.text(); err != nil {
115				return err
116			}
117			continue
118		}
119		if p.i+1 >= len(p.s) {
120			return svgErr("unterminated tag")
121		}
122		c := p.s[p.i+1]
123		switch {
124		case c == '!':
125			return svgErr("comments, CDATA, doctype and entity declarations are not allowed")
126		case c == '?':
127			return svgErr("processing instructions are not allowed")
128		case c == '/':
129			if err := p.closeTag(); err != nil {
130				return err
131			}
132			if len(p.stack) == 0 {
133				rootDone = true
134			}
135		default:
136			first := len(p.stack) == 0
137			name, selfClosed, err := p.openTag()
138			if err != nil {
139				return err
140			}
141			if first && name != "svg" {
142				return svgErr("the root element must be <svg>")
143			}
144			if selfClosed && len(p.stack) == 0 {
145				rootDone = true
146			}
147		}
148	}
149	if len(p.stack) != 0 {
150		return svgErr("unclosed element <" + p.stack[len(p.stack)-1] + ">")
151	}
152	if !rootDone {
153		return svgErr("no root element")
154	}
155	return nil
156}
157
158// text is character data: printable characters, whitespace and the five
159// XML entities only.
160func (p *svgParser) text() error {
161	if len(p.stack) == 0 {
162		return svgErr("text outside the root element")
163	}
164	start := p.i
165	for p.i < len(p.s) && p.s[p.i] != '<' {
166		p.i++
167	}
168	return checkChars(p.s[start:p.i], true)
169}
170
171func (p *svgParser) openTag() (string, bool, error) {
172	p.i++ // '<'
173	name := p.name()
174	if name == "" {
175		return "", false, svgErr("malformed tag")
176	}
177	if strings.Contains(name, ":") || !svgElements[name] {
178		return "", false, svgErr("element <" + name + "> is not allowed")
179	}
180	seen := map[string]bool{}
181	for {
182		sp := p.spaces()
183		if p.i >= len(p.s) {
184			return "", false, svgErr("unterminated tag <" + name + ">")
185		}
186		switch p.s[p.i] {
187		case '>':
188			p.i++
189			p.stack = append(p.stack, name)
190			return name, false, nil
191		case '/':
192			if p.i+1 >= len(p.s) || p.s[p.i+1] != '>' {
193				return "", false, svgErr("malformed tag <" + name + ">")
194			}
195			p.i += 2
196			return name, true, nil
197		}
198		if !sp {
199			return "", false, svgErr("attributes must be separated by whitespace in <" + name + ">")
200		}
201		attr := p.name()
202		if attr == "" {
203			return "", false, svgErr("malformed attribute in <" + name + ">")
204		}
205		if seen[attr] {
206			return "", false, svgErr("duplicate attribute " + attr)
207		}
208		seen[attr] = true
209		p.spaces()
210		if p.i >= len(p.s) || p.s[p.i] != '=' {
211			return "", false, svgErr("attribute " + attr + " needs a value")
212		}
213		p.i++
214		p.spaces()
215		if p.i >= len(p.s) || (p.s[p.i] != '"' && p.s[p.i] != '\'') {
216			return "", false, svgErr("attribute " + attr + " must be quoted")
217		}
218		q := p.s[p.i]
219		p.i++
220		start := p.i
221		for p.i < len(p.s) && p.s[p.i] != q {
222			p.i++
223		}
224		if p.i >= len(p.s) {
225			return "", false, svgErr("unterminated value of " + attr)
226		}
227		val := p.s[start:p.i]
228		p.i++
229		if err := checkAttr(name, attr, val); err != nil {
230			return "", false, err
231		}
232	}
233}
234
235func (p *svgParser) closeTag() error {
236	p.i += 2 // "</"
237	name := p.name()
238	p.spaces()
239	if p.i >= len(p.s) || p.s[p.i] != '>' {
240		return svgErr("malformed closing tag")
241	}
242	p.i++
243	if len(p.stack) == 0 || p.stack[len(p.stack)-1] != name {
244		return svgErr("mismatched closing tag </" + name + ">")
245	}
246	p.stack = p.stack[:len(p.stack)-1]
247	return nil
248}
249
250// name reads an XML name made of ASCII letters, digits, '-', '_', '.', ':'.
251func (p *svgParser) name() string {
252	start := p.i
253	for p.i < len(p.s) {
254		c := p.s[p.i]
255		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' ||
256			(p.i > start && ((c >= '0' && c <= '9') || c == '-' || c == '.')) {
257			p.i++
258			continue
259		}
260		break
261	}
262	return p.s[start:p.i]
263}
264
265func (p *svgParser) spaces() bool {
266	start := p.i
267	for p.i < len(p.s) && isSpace(p.s[p.i]) {
268		p.i++
269	}
270	return p.i > start
271}
272
273func checkAttr(elem, attr, val string) error {
274	if !svgAttributes[attr] {
275		return svgErr("attribute " + attr + " is not allowed")
276	}
277	switch attr {
278	case "xmlns":
279		if val != svgNS {
280			return svgErr("xmlns must be " + svgNS)
281		}
282		return nil
283	case "xmlns:xlink":
284		if val != xlinkNS {
285			return svgErr("xmlns:xlink must be " + xlinkNS)
286		}
287		return nil
288	}
289	if err := checkChars(val, false); err != nil {
290		return svgErr("attribute " + attr + ": " + err.Error())
291	}
292	if strings.ContainsAny(val, ":\\<") {
293		return svgErr("attribute " + attr + " contains a forbidden character (':', '\\' or '<')")
294	}
295	if attr == "href" || attr == "xlink:href" {
296		if !isFragment(val) {
297			return svgErr("references must stay inside the document (#id)")
298		}
299		return nil
300	}
301	low := strings.ToLower(val)
302	for {
303		k := strings.Index(low, "url")
304		if k < 0 {
305			return nil
306		}
307		rest := strings.TrimLeft(low[k+3:], " ")
308		if !strings.HasPrefix(rest, "(") {
309			low = low[k+3:]
310			continue
311		}
312		rest = strings.TrimLeft(rest[1:], " ")
313		end := strings.IndexByte(rest, ')')
314		if end < 0 || !isFragment(strings.TrimRight(rest[:end], " ")) {
315			return svgErr("references must stay inside the document (url(#id))")
316		}
317		low = rest[end:]
318	}
319}
320
321// checkChars allows printable characters and the five XML entities; text
322// (character data) may also hold whitespace, attribute values only spaces.
323func checkChars(s string, isText bool) error {
324	for i := 0; i < len(s); i++ {
325		c := s[i]
326		switch {
327		case c == '&':
328			ok := false
329			for _, e := range []string{"&amp;", "&lt;", "&gt;", "&quot;", "&apos;"} {
330				if strings.HasPrefix(s[i:], e) {
331					ok = true
332					break
333				}
334			}
335			if !ok {
336				return svgErr("only &amp; &lt; &gt; &quot; &apos; entities are allowed")
337			}
338		case c == 0x7f || (c < 0x20 && !(isText && isSpace(c))):
339			return svgErr("control characters are not allowed")
340		case c == '>' && !isText:
341			return svgErr("'>' in an attribute value")
342		}
343	}
344	for _, r := range s {
345		if r >= 0x80 && r <= 0x9f {
346			return svgErr("control characters are not allowed")
347		}
348	}
349	return nil
350}
351
352func isFragment(v string) bool {
353	if len(v) < 2 || v[0] != '#' {
354		return false
355	}
356	for i := 1; i < len(v); i++ {
357		c := v[i]
358		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') {
359			return false
360		}
361	}
362	return true
363}
364
365func isSpace(c byte) bool {
366	return c == ' ' || c == '\t' || c == '\n' || c == '\r'
367}