From 7477aec06c9870a719549b894911c3b219811284 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Tue, 27 Apr 2021 10:41:42 +0200 Subject: [PATCH] Upgrade to 3.5.4 --- kirby/composer.json | 2 +- kirby/composer.lock | 2 +- kirby/src/Cms/FileRules.php | 2 + kirby/src/Sane/Handler.php | 50 ++ kirby/src/Sane/Sane.php | 128 ++++++ kirby/src/Sane/Svg.php | 485 ++++++++++++++++++++ kirby/src/Sane/Svgz.php | 38 ++ kirby/src/Sane/Xml.php | 338 ++++++++++++++ kirby/src/Toolkit/File.php | 18 + kirby/vendor/composer/InstalledVersions.php | 8 +- kirby/vendor/composer/installed.php | 8 +- 11 files changed, 1069 insertions(+), 10 deletions(-) create mode 100755 kirby/src/Sane/Handler.php create mode 100755 kirby/src/Sane/Sane.php create mode 100755 kirby/src/Sane/Svg.php create mode 100755 kirby/src/Sane/Svgz.php create mode 100755 kirby/src/Sane/Xml.php diff --git a/kirby/composer.json b/kirby/composer.json index 501fa7f..7b427f7 100755 --- a/kirby/composer.json +++ b/kirby/composer.json @@ -8,7 +8,7 @@ "core" ], "homepage": "https://getkirby.com", - "version": "3.5.3.1", + "version": "3.5.4", "license": "proprietary", "authors": [ { diff --git a/kirby/composer.lock b/kirby/composer.lock index 5b06005..bd46947 100755 --- a/kirby/composer.lock +++ b/kirby/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "73d01b459246b298ff50d068f6b909af", + "content-hash": "70ee865c7f7cf618466ffb0f90bca1d8", "packages": [ { "name": "claviska/simpleimage", diff --git a/kirby/src/Cms/FileRules.php b/kirby/src/Cms/FileRules.php index f312413..3929b07 100755 --- a/kirby/src/Cms/FileRules.php +++ b/kirby/src/Cms/FileRules.php @@ -85,6 +85,7 @@ class FileRules static::validFile($file, $upload->mime()); $upload->match($file->blueprint()->accept()); + $upload->validateContents(true); return true; } @@ -133,6 +134,7 @@ class FileRules } $upload->match($file->blueprint()->accept()); + $upload->validateContents(true); return true; } diff --git a/kirby/src/Sane/Handler.php b/kirby/src/Sane/Handler.php new file mode 100755 index 0000000..0178f98 --- /dev/null +++ b/kirby/src/Sane/Handler.php @@ -0,0 +1,50 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Handler +{ + /** + * Validates file contents + * + * @param string $string + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\Exception On other errors + */ + abstract public static function validate(string $string): void; + + /** + * Validates the contents of a file + * + * @param string $file + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validateFile(string $file): void + { + $contents = F::read($file); + if ($contents === false) { + throw new Exception('The file "' . $file . '" does not exist'); + } + + static::validate($contents); + } +} diff --git a/kirby/src/Sane/Sane.php b/kirby/src/Sane/Sane.php new file mode 100755 index 0000000..cd4c63e --- /dev/null +++ b/kirby/src/Sane/Sane.php @@ -0,0 +1,128 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sane +{ + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'image/svg+xml' => 'svg', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + ]; + + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'svg' => 'Kirby\Sane\Svg', + 'svgz' => 'Kirby\Sane\Svgz', + 'xml' => 'Kirby\Sane\Xml', + ]; + + /** + * Handler getter + * + * @param string $type + * @param bool $lazy If set to `true`, `null` is returned for undefined handlers + * @return \Kirby\Sane\Handler|null + * + * @throws \Kirby\Exception\NotFoundException If no handler was found and `$lazy` was set to `false` + */ + public static function handler(string $type, bool $lazy = false) + { + // normalize the type + $type = mb_strtolower($type); + + // find a handler or alias + $handler = static::$handlers[$type] ?? + static::$handlers[static::$aliases[$type] ?? null] ?? + null; + + if (empty($handler) === false && class_exists($handler) === true) { + return new $handler(); + } + + if ($lazy === true) { + return null; + } + + throw new NotFoundException('Missing handler for type: "' . $type . '"'); + } + + /** + * Validates file contents with the specified handler + * + * @param mixed $string + * @param string $type + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validate(string $string, string $type): void + { + static::handler($type)->validate($string); + } + + /** + * Validates the contents of a file; + * the sane handlers are automatically chosen by + * the extension and MIME type if not specified + * + * @param string $file + * @param string|bool $typeLazy Explicit handler type string, + * `true` for lazy autodetection or + * `false` for normal autodetection + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validateFile(string $file, $typeLazy = false): void + { + if (is_string($typeLazy) === true) { + static::handler($typeLazy)->validateFile($file); + return; + } + + $options = [F::extension($file), F::mime($file)]; + + // execute all handlers, but each class only once for performance; + // filter out all empty options + $usedHandlers = []; + foreach (array_filter($options) as $option) { + $handler = static::handler($option, $typeLazy === true); + $handlerClass = $handler ? get_class($handler) : null; + + if ($handler && in_array($handlerClass, $usedHandlers) === false) { + $handler->validateFile($file); + + $usedHandlers[] = $handlerClass; + } + } + } +} diff --git a/kirby/src/Sane/Svg.php b/kirby/src/Sane/Svg.php new file mode 100755 index 0000000..f2fe870 --- /dev/null +++ b/kirby/src/Sane/Svg.php @@ -0,0 +1,485 @@ +, + * Lukas Bestle + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @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 + */ + public static $allowedAttributes = [ + 'accent-height', + 'accumulate', + 'additive', + 'alignment-baseline', + 'ascent', + 'attributeName', + 'attributeType', + 'azimuth', + 'baseFrequency', + 'baseline-shift', + 'begin', + 'bias', + 'by', + 'class', + 'clip', + 'clipPathUnits', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cx', + 'cy', + 'd', + 'dx', + 'dy', + 'diffuseConstant', + 'direction', + 'display', + 'divisor', + 'dur', + 'edgeMode', + 'elevation', + 'end', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'filterUnits', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'fx', + 'fy', + 'g1', + 'g2', + 'glyph-name', + 'glyphRef', + 'gradientUnits', + 'gradientTransform', + 'height', + 'href', + 'id', + 'image-rendering', + 'in', + 'in2', + 'k', + 'k1', + 'k2', + 'k3', + 'k4', + 'kerning', + 'keyPoints', + 'keySplines', + 'keyTimes', + 'lang', + 'lengthAdjust', + 'letter-spacing', + 'kernelMatrix', + 'kernelUnitLength', + 'lighting-color', + 'local', + 'marker-end', + 'marker-mid', + 'marker-start', + 'markerHeight', + 'markerUnits', + 'markerWidth', + 'maskContentUnits', + 'maskUnits', + 'max', + 'mask', + 'media', + 'method', + 'mode', + 'min', + 'name', + 'numOctaves', + 'offset', + 'operator', + 'opacity', + 'order', + 'orient', + 'orientation', + 'origin', + 'overflow', + 'paint-order', + 'path', + 'pathLength', + 'patternContentUnits', + 'patternTransform', + 'patternUnits', + 'points', + 'preserveAlpha', + 'preserveAspectRatio', + 'primitiveUnits', + 'r', + 'rx', + 'ry', + 'radius', + 'refX', + 'refY', + 'repeatCount', + 'repeatDur', + 'restart', + 'result', + 'rotate', + 'scale', + 'seed', + 'shape-rendering', + 'specularConstant', + 'specularExponent', + 'spreadMethod', + 'startOffset', + 'stdDeviation', + 'stitchTiles', + 'stop-color', + 'stop-opacity', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke', + 'stroke-width', + 'style', + 'surfaceScale', + 'systemLanguage', + 'tabindex', + 'targetX', + 'targetY', + 'transform', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'textLength', + 'type', + 'u1', + 'u2', + 'unicode', + 'values', + 'viewBox', + 'visibility', + 'version', + 'vert-adv-y', + 'vert-origin-x', + 'vert-origin-y', + 'width', + 'word-spacing', + 'wrap', + 'writing-mode', + 'xChannelSelector', + 'yChannelSelector', + 'x', + 'x1', + 'x2', + 'xlink:href', + 'y', + 'y1', + 'y2', + 'z', + 'zoomAndPan', + ]; + + public static $allowedElements = [ + 'svg', + 'a', + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'circle', + 'clipPath', + 'defs', + 'desc', + 'ellipse', + 'filter', + 'font', + 'g', + 'glyph', + 'glyphRef', + 'hkern', + 'image', + 'line', + 'linearGradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialGradient', + 'rect', + 'stop', + 'style', + 'switch', + 'symbol', + 'text', + 'textPath', + 'title', + 'tref', + 'tspan', + 'use', + 'view', + 'vkern', + ]; + + public static $allowedFilters = [ + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + ]; + + protected static $allowedNamespaces = [ + 'xmlns' => 'http://www.w3.org/2000/svg', + 'xmlns:svg' => 'http://www.w3.org/2000/svg', + 'xmlns:xlink' => 'http://www.w3.org/1999/xlink' + ]; + + /** + * IMPORTANT: Use lower-case names here because + * of the case-insensitive matching + */ + public static $disallowedElements = [ + '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', + ]; + + /** + * Validates file contents + * + * @param string $string + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + */ + public static function validate(string $string): void + { + $svg = static::parse($string); + + $rootName = $svg->documentElement->nodeName; + if ($rootName !== 'svg') { + throw new InvalidArgumentException('The file is not a SVG (got <' . $rootName . '>)'); + } + + parent::validateDom($svg); + } + + /** + * Validates the attributes of an element + * + * @param \DOMXPath $xPath + * @param \DOMNode $element + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If any of the attributes is not valid + */ + protected static function validateAttrs(DOMXPath $xPath, DOMNode $element): void + { + $elementName = $element->nodeName; + + foreach ($element->attributes ?? [] as $attr) { + $attrName = $attr->nodeName; + $attrValue = $attr->nodeValue; + + // allow all aria and data attributes + $beginning = mb_substr($attrName, 0, 5); + if ($beginning === 'aria-' || $beginning === 'data-') { + continue; + } + + if (in_array($attrName, static::$allowedAttributes) !== true) { + throw new InvalidArgumentException( + 'The "' . $attrName . '" attribute (line ' . + $attr->getLineNo() . ') is not allowed in SVGs' + ); + } + + // block nested elements ("Billion Laughs" DoS attack) + if ( + $elementName === 'use' && + Str::contains($attrName, 'href') !== false && + Str::startsWith($attrValue, '#') === true + ) { + // find the target (used element) + $id = str_replace('"', '', mb_substr($attrValue, 1)); + $target = $xPath->query('//*[@id="' . $id . '"]')->item(0); + + // the target must not contain any other elements + if ( + is_a($target, 'DOMElement') === true && + $target->getElementsByTagName('use')->count() > 0 + ) { + throw new InvalidArgumentException( + 'Nested "use" elements are not allowed in SVGs (used in line ' . + $element->getLineNo() . ')' + ); + } + } + } + + // validate `xmlns` attributes as well, which can only + // be properly extracted using SimpleXML + if (is_a($element, 'DOMElement') === true) { + $simpleXmlElement = simplexml_import_dom($element); + foreach ($simpleXmlElement->getDocNamespaces(false, false) as $namespace => $value) { + $namespace = 'xmlns' . ($namespace ? ':' . $namespace : ''); + + // check if the namespace is allowlisted + if ( + isset(static::$allowedNamespaces[$namespace]) !== true || + static::$allowedNamespaces[$namespace] !== $value + ) { + throw new InvalidArgumentException( + 'The namespace "' . $namespace . '" (around line ' . + $element->getLineNo() . ') is not allowed or has an invalid value' + ); + } + } + } + + parent::validateAttrs($xPath, $element); + } + + /** + * Validates the doctype if present + * + * @param \DOMDocumentType $doctype + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the doctype is not valid + */ + protected static function validateDoctype(DOMDocumentType $doctype): void + { + if (mb_strtolower($doctype->name) !== 'svg') { + throw new InvalidArgumentException('Invalid doctype'); + } + + parent::validateDoctype($doctype); + } + + /** + * Validates all given DOM elements and their attributes + * + * @param \DOMXPath $xPath + * @param \DOMNodeList $elements + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If any of the elements is not valid + */ + protected static function validateElements(DOMXPath $xPath, DOMNodeList $elements): void + { + $allowedElements = array_merge(static::$allowedElements, static::$allowedFilters); + + foreach ($elements as $element) { + $elementName = $element->nodeName; + $elementNameLower = mb_strtolower($elementName); + + // check for block-listed elements + if (in_array($elementNameLower, static::$disallowedElements) === true) { + throw new InvalidArgumentException( + 'The "' . $elementName . '" element (line ' . + $element->getLineNo() . ') is not allowed in SVGs' + ); + } + + // check for allow-listed elements + if (in_array($elementName, $allowedElements) === false) { + throw new InvalidArgumentException( + 'The "' . $elementName . '" element (line ' . + $element->getLineNo() . ') is not allowed in SVGs' + ); + } + + // check for URLs inside