SvgSanitizer
in package
Sanitizes SVG to a safe subset before the file reaches `www/`.
SVG is XML that can run scripts: <script>, on* handlers, <foreignObject> with HTML,
href="javascript:", CSS url(). Assets are served by the web server straight from the file,
so an SVG opened on its own URL runs as a same-origin document on the domain where the admin
has a session cookie. Only an admin can upload, but the victim of a stored script is again
an admin.
Hence a whitelist, not a blacklist: whatever is not in ALLOWED_ELEMENTS /
ALLOWED_ATTRIBUTES is removed. The lists cover common icon exports (shapes, gradients,
masks, filters); <image> and <a> are deliberately absent because icons do not need them
and both can link outside.
Table of Contents
Constants
- ALLOWED_ATTRIBUTES : array<string|int, mixed> = ['id', 'class', 'style', 'transform', 'd', 'poi...
- ALLOWED_ELEMENTS : array<string|int, mixed> = ['svg', 'g', 'defs', 'symbol', 'use', 'title',...
- DANGEROUS_CSS : string = '~(?:url\s*\(\s*[\'"]?(?!\#)|expression\s*\(|@i...
- Values in attributes and in `<style>` that have no business in an icon.
Methods
- getDimensions() : array{width: int, height: int}|null
- Dimensions from the root `<svg>`: `width`/`height`, otherwise the 3rd and 4th `viewBox` value.
- sanitize() : string|null
- Returns the sanitized SVG, or null when the input is not a usable SVG.
- cleanElement() : void
- Cleans the attributes of one element and the content of `<style>`.
- cleanNode() : void
- Recursively walks the tree and removes disallowed nodes and attributes.
- isDangerousCss() : bool
- Normalizes a value to the form the CSS tokenizer sees, before the {@see DANGEROUS_CSS} mask runs.
- normalizeCss() : string
- CSS Syntax Level 3, section 4.3.7 "consume an escaped code point": `\` plus 1-6 hex digits (and one optional space) is a code point; `\` plus a line break is a line continuation (nothing); `\` plus any other character is that character. CSS comments (even an unterminated one at the end) are dropped.
- parseLength() : int|null
- `"32"`, `"32px"` and `"32.5pt"` give 32; percentages and other relative units give null.
Constants
ALLOWED_ATTRIBUTES
private
array<string|int, mixed>
ALLOWED_ATTRIBUTES
= ['id', 'class', 'style', 'transform', 'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'dx', 'dy', 'width', 'height', 'viewbox', 'preserveaspectratio', 'version', 'fill', 'fill-opacity', 'fill-rule', 'clip-rule', 'clip-path', 'mask', 'filter', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity', 'stroke-miterlimit', 'opacity', 'color', 'display', 'visibility', 'overflow', 'offset', 'stop-color', 'stop-opacity', 'spreadmethod', 'gradientunits', 'gradienttransform', 'patternunits', 'patterncontentunits', 'maskunits', 'maskcontentunits', 'clippathunits', 'filterunits', 'primitiveunits', 'markerwidth', 'markerheight', 'refx', 'refy', 'orient', 'markerunits', 'font-family', 'font-size', 'font-weight', 'font-style', 'letter-spacing', 'text-anchor', 'dominant-baseline', 'baseline-shift', 'in', 'in2', 'result', 'stddeviation', 'mode', 'values', 'type', 'operator', 'k1', 'k2', 'k3', 'k4', 'flood-color', 'flood-opacity', 'radius', 'href', 'xlink:href', 'xml:space']
ALLOWED_ELEMENTS
private
array<string|int, mixed>
ALLOWED_ELEMENTS
= ['svg', 'g', 'defs', 'symbol', 'use', 'title', 'desc', 'style', 'metadata', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'tspan', 'textpath', 'lineargradient', 'radialgradient', 'stop', 'pattern', 'clippath', 'mask', 'marker', 'filter', 'fegaussianblur', 'feoffset', 'feblend', 'fecolormatrix', 'fecomposite', 'feflood', 'femerge', 'femergenode', 'fedropshadow', 'femorphology']
DANGEROUS_CSS
Values in attributes and in `<style>` that have no business in an icon.
private
string
DANGEROUS_CSS
= '~(?:url\s*\(\s*[\'"]?(?!\#)|expression\s*\(|@import|javascript\s*:|behavior\s*:|-moz-binding)~i'
url(#...) is deliberately allowed: a reference to a gradient or mask inside the same
document (fill: url(#grad)) is common in exported icons. A url() pointing anywhere else
(external resource, data:, javascript:) is blocked.
IMPORTANT: the mask runs on every attribute, not only on style. Presentation attributes take
url() exactly like CSS, so filter="url(https://example.com/x.svg#f)" or
fill="url(//example.com/g.svg#a)" would pass unchecked; a link out of an icon makes no sense
and is a leak channel.
Methods
getDimensions()
Dimensions from the root `<svg>`: `width`/`height`, otherwise the 3rd and 4th `viewBox` value.
public
getDimensions(string $svg) : array{width: int, height: int}|null
Used only for Asset.meta; for a vector it is informational, not the file dimensions.
Parameters
- $svg : string
Return values
array{width: int, height: int}|nullsanitize()
Returns the sanitized SVG, or null when the input is not a usable SVG.
public
sanitize(string $svg) : string|null
Null means "do not accept it" and the caller discards the file. Reasons: invalid XML, the root
is not <svg>, or an internal DTD subset (the way entity bombs and XXE get in).
Parameters
- $svg : string
Return values
string|nullcleanElement()
Cleans the attributes of one element and the content of `<style>`.
private
cleanElement(DOMElement $element) : void
Parameters
- $element : DOMElement
cleanNode()
Recursively walks the tree and removes disallowed nodes and attributes.
private
cleanNode(DOMNode $node) : void
Iterates over a copy of the child list; deleting during iteration would make the live NodeList skip nodes.
Parameters
- $node : DOMNode
isDangerousCss()
Normalizes a value to the form the CSS tokenizer sees, before the {@see DANGEROUS_CSS} mask runs.
private
isDangerousCss(string $value) : bool
IMPORTANT: running the mask on raw text lets a CSS escape such as
@\69 mport "https://example.com/x.css" through (the browser reads \69 as i, giving
@import). So escapes are decoded and comments dropped first. Dropping comments is
deliberately fail-closed: @im, a comment, port is not an @import for a browser, but an
icon needs no such form, so rejecting it is safer than relying on an exact match with the
browser tokenizer.
Parameters
- $value : string
Return values
boolnormalizeCss()
CSS Syntax Level 3, section 4.3.7 "consume an escaped code point": `\` plus 1-6 hex digits (and one optional space) is a code point; `\` plus a line break is a line continuation (nothing); `\` plus any other character is that character. CSS comments (even an unterminated one at the end) are dropped.
private
normalizeCss(string $value) : string
Parameters
- $value : string
Return values
stringparseLength()
`"32"`, `"32px"` and `"32.5pt"` give 32; percentages and other relative units give null.
private
parseLength(string $value) : int|null
Parameters
- $value : string