, * Lukas Bestle * @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Svg extends Xml { /** * Allow and block lists are inspired by DOMPurify * * @link https://github.com/cure53/DOMPurify * @copyright 2015 Mario Heiderich * @license https://www.apache.org/licenses/LICENSE-2.0 */ /** * Global list of allowed attribute prefixes * * @var array */ public static $allowedAttrPrefixes = [ 'aria-', 'data-', ]; /** * Global list of allowed attributes * * @var array */ public static $allowedAttrs = [ // core attributes 'id', 'lang', 'tabindex', 'xml:id', 'xml:lang', 'xml:space', // styling attributes 'class', 'style', // conditional processing attributes 'systemLanguage', // presentation attributes 'alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'd', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'paint-order', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'transform', 'visibility', 'word-spacing', 'writing-mode', // animation attribute target attributes 'attributeName', 'attributeType', // animation timing attributes 'begin', 'dur', 'end', 'max', 'min', 'repeatCount', 'repeatDur', 'restart', // animation value attributes 'by', 'from', 'keySplines', 'keyTimes', 'to', 'values', // animation addition attributes 'accumulate', 'additive', // filter primitive attributes 'height', 'result', 'width', 'x', 'y', // transfer function attributes 'amplitude', 'exponent', 'intercept', 'offset', 'slope', 'tableValues', 'type', // other attributes specific to one or multiple elements 'azimuth', 'baseFrequency', 'bias', 'clipPathUnits', 'cx', 'cy', 'diffuseConstant', 'divisor', 'dx', 'dy', 'edgeMode', 'elevation', 'filterUnits', 'fr', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphRef', 'gradientTransform', 'gradientUnits', 'href', 'hreflang', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kernelMatrix', 'kernelUnitLength', 'keyPoints', 'lengthAdjust', 'limitingConeAngle', 'markerHeight', 'markerUnits', 'markerWidth', 'maskContentUnits', 'maskUnits', 'media', 'method', 'mode', 'numOctaves', 'operator', 'order', 'orient', 'orientation', 'path', 'pathLength', 'patternContentUnits', 'patternTransform', 'patternUnits', 'points', 'pointsAtX', 'pointsAtY', 'pointsAtZ', 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'r', 'radius', 'refX', 'refY', 'rotate', 'rx', 'ry', 'scale', 'seed', 'side', 'spacing', 'specularConstant', 'specularExponent', 'spreadMethod', 'startOffset', 'stdDeviation', 'stitchTiles', 'surfaceScale', 'targetX', 'targetY', 'textLength', 'u1', 'u2', 'unicode', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'viewBox', 'x1', 'x2', 'xChannelSelector', 'xlink:href', 'xlink:title', 'y1', 'y2', 'yChannelSelector', 'z', 'zoomAndPan', ]; /** * Associative array of all allowed namespace URIs * * @var array */ public static $allowedNamespaces = [ '' => 'http://www.w3.org/2000/svg', 'xlink' => 'http://www.w3.org/1999/xlink' ]; /** * Associative array of all allowed tag names with the value * of either an array with the list of all allowed attributes * for this tag, `true` to allow any attribute from the * `allowedAttrs` list or `false` to allow the tag without * any attributes * * @var array */ public static $allowedTags = [ 'a' => true, 'altGlyph' => true, 'altGlyphDef' => true, 'altGlyphItem' => true, 'animateColor' => true, 'animateMotion' => true, 'animateTransform' => true, 'circle' => true, 'clipPath' => true, 'defs' => true, 'desc' => true, 'ellipse' => true, 'feBlend' => true, 'feColorMatrix' => true, 'feComponentTransfer' => true, 'feComposite' => true, 'feConvolveMatrix' => true, 'feDiffuseLighting' => true, 'feDisplacementMap' => true, 'feDistantLight' => true, 'feFlood' => true, 'feFuncA' => true, 'feFuncB' => true, 'feFuncG' => true, 'feFuncR' => true, 'feGaussianBlur' => true, 'feMerge' => true, 'feMergeNode' => true, 'feMorphology' => true, 'feOffset' => true, 'fePointLight' => true, 'feSpecularLighting' => true, 'feSpotLight' => true, 'feTile' => true, 'feTurbulence' => true, 'filter' => true, 'font' => true, 'g' => true, 'glyph' => true, 'glyphRef' => true, 'hkern' => true, 'image' => true, 'line' => true, 'linearGradient' => true, 'marker' => true, 'mask' => true, 'metadata' => true, 'mpath' => true, 'path' => true, 'pattern' => true, 'polygon' => true, 'polyline' => true, 'radialGradient' => true, 'rect' => true, 'stop' => true, 'style' => true, 'svg' => true, 'switch' => true, 'symbol' => true, 'text' => true, 'textPath' => true, 'title' => true, 'tref' => true, 'tspan' => true, 'use' => true, 'view' => true, 'vkern' => true, ]; /** * Array of explicitly disallowed tags * * IMPORTANT: Use lower-case names here because * of the case-insensitive matching * * @var array */ public static $disallowedTags = [ 'animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', 'feimage', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', ]; /** * Custom callback for additional attribute sanitization * @internal * * @param \DOMAttr $attr * @return array Array with exception objects for each modification */ public static function sanitizeAttr(DOMAttr $attr): array { $element = $attr->ownerElement; $name = $attr->name; $value = $attr->value; $errors = []; // block nested elements ("Billion Laughs" DoS attack) if ( $element->localName === 'use' && Str::contains($name, 'href') !== false && Str::startsWith($value, '#') === true ) { // find the target (used element) $id = str_replace('"', '', mb_substr($value, 1)); $target = (new DOMXPath($attr->ownerDocument))->query('//*[@id="' . $id . '"]')->item(0); // the target must not contain any other elements if ( is_a($target, 'DOMElement') === true && $target->getElementsByTagName('use')->count() > 0 ) { $errors[] = new InvalidArgumentException( 'Nested "use" elements are not allowed' . ' (used in line ' . $element->getLineNo() . ')' ); $element->removeAttributeNode($attr); } } return $errors; } /** * Custom callback for additional element sanitization * @internal * * @param \DOMElement $element * @return array Array with exception objects for each modification */ public static function sanitizeElement(DOMElement $element): array { $errors = []; // check for URLs inside