Initial commit
This commit is contained in:
commit
73c6b816c0
716 changed files with 170045 additions and 0 deletions
165
kirby/src/Sane/DomHandler.php
Normal file
165
kirby/src/Sane/DomHandler.php
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use DOMAttr;
|
||||
use DOMDocumentType;
|
||||
use DOMElement;
|
||||
use Kirby\Toolkit\Dom;
|
||||
|
||||
/**
|
||||
* Base class for Sane handlers with DOM file types
|
||||
* @since 3.5.8
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class DomHandler extends Handler
|
||||
{
|
||||
/**
|
||||
* List of all MIME types that may
|
||||
* be used in data URIs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedDataUris = [
|
||||
'data:image/png',
|
||||
'data:image/gif',
|
||||
'data:image/jpg',
|
||||
'data:image/jpe',
|
||||
'data:image/pjp',
|
||||
'data:img/png',
|
||||
'data:img/gif',
|
||||
'data:img/jpg',
|
||||
'data:img/jpe',
|
||||
'data:img/pjp',
|
||||
];
|
||||
|
||||
/**
|
||||
* Allowed hostnames for HTTP(S) URLs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedDomains = [];
|
||||
|
||||
/**
|
||||
* Names of allowed XML processing instructions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedPIs = [];
|
||||
|
||||
/**
|
||||
* The document type (`'HTML'` or `'XML'`)
|
||||
* (to be set in child classes)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $type = 'XML';
|
||||
|
||||
/**
|
||||
* Sanitizes the given string
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||
*/
|
||||
public static function sanitize(string $string): string
|
||||
{
|
||||
$dom = static::parse($string);
|
||||
$dom->sanitize(static::options());
|
||||
return $dom->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file contents
|
||||
*
|
||||
* @param string $string
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||
*/
|
||||
public static function validate(string $string): void
|
||||
{
|
||||
$dom = static::parse($string);
|
||||
$errors = $dom->sanitize(static::options());
|
||||
if (count($errors) > 0) {
|
||||
// there may be multiple errors, we can only throw one of them at a time
|
||||
throw $errors[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// to be extended in child classes
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// to be extended in child classes
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom callback for additional doctype validation
|
||||
* @internal
|
||||
*
|
||||
* @param \DOMDocumentType $doctype
|
||||
* @return void
|
||||
*/
|
||||
public static function validateDoctype(DOMDocumentType $doctype): void
|
||||
{
|
||||
// to be extended in child classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sanitization options for the handler
|
||||
* (to be extended in child classes)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function options(): array
|
||||
{
|
||||
return [
|
||||
'allowedDataUris' => static::$allowedDataUris,
|
||||
'allowedDomains' => static::$allowedDomains,
|
||||
'allowedPIs' => static::$allowedPIs,
|
||||
'attrCallback' => [static::class, 'sanitizeAttr'],
|
||||
'doctypeCallback' => [static::class, 'validateDoctype'],
|
||||
'elementCallback' => [static::class, 'sanitizeElement'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given string into a `Toolkit\Dom` object
|
||||
*
|
||||
* @param string $string
|
||||
* @return \Kirby\Toolkit\Dom
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||
*/
|
||||
protected static function parse(string $string)
|
||||
{
|
||||
return new Dom($string, static::$type);
|
||||
}
|
||||
}
|
91
kirby/src/Sane/Handler.php
Normal file
91
kirby/src/Sane/Handler.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Filesystem\F;
|
||||
|
||||
/**
|
||||
* Base handler abstract,
|
||||
* which needs to be extended to
|
||||
* create valid sane handlers
|
||||
* @since 3.5.4
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
abstract class Handler
|
||||
{
|
||||
/**
|
||||
* Sanitizes the given string
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
abstract public static function sanitize(string $string): string;
|
||||
|
||||
/**
|
||||
* Sanitizes the contents of a file by overwriting
|
||||
* the file with the sanitized version
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception If the file does not exist
|
||||
* @throws \Kirby\Exception\Exception On other errors
|
||||
*/
|
||||
public static function sanitizeFile(string $file): void
|
||||
{
|
||||
$sanitized = static::sanitize(static::readFile($file));
|
||||
F::write($file, $sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 If the file does not exist
|
||||
* @throws \Kirby\Exception\Exception On other errors
|
||||
*/
|
||||
public static function validateFile(string $file): void
|
||||
{
|
||||
static::validate(static::readFile($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of a file
|
||||
* for sanitization or validation
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception If the file does not exist
|
||||
*/
|
||||
protected static function readFile(string $file): string
|
||||
{
|
||||
$contents = F::read($file);
|
||||
|
||||
if ($contents === false) {
|
||||
throw new Exception('The file "' . $file . '" does not exist');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
144
kirby/src/Sane/Html.php
Normal file
144
kirby/src/Sane/Html.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
/**
|
||||
* Sane handler for HTML files
|
||||
* @since 3.5.8
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Html extends DomHandler
|
||||
{
|
||||
/**
|
||||
* Global list of allowed attribute prefixes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedAttrPrefixes = [
|
||||
'aria-',
|
||||
'data-',
|
||||
];
|
||||
|
||||
/**
|
||||
* Global list of allowed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedAttrs = [
|
||||
'class',
|
||||
'id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Allowed hostnames for HTTP(S) URLs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedDomains = true;
|
||||
|
||||
/**
|
||||
* 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' => ['href', 'rel', 'title', 'target'],
|
||||
'abbr' => ['title'],
|
||||
'b' => true,
|
||||
'body' => true,
|
||||
'blockquote' => true,
|
||||
'br' => true,
|
||||
'code' => true,
|
||||
'dl' => true,
|
||||
'dd' => true,
|
||||
'del' => true,
|
||||
'div' => true,
|
||||
'dt' => true,
|
||||
'em' => true,
|
||||
'footer' => true,
|
||||
'h1' => true,
|
||||
'h2' => true,
|
||||
'h3' => true,
|
||||
'h4' => true,
|
||||
'h5' => true,
|
||||
'h6' => true,
|
||||
'hr' => true,
|
||||
'html' => true,
|
||||
'i' => true,
|
||||
'ins' => true,
|
||||
'li' => true,
|
||||
'small' => true,
|
||||
'span' => true,
|
||||
'strong' => true,
|
||||
'sub' => true,
|
||||
'sup' => true,
|
||||
'ol' => true,
|
||||
'p' => true,
|
||||
'pre' => true,
|
||||
's' => true,
|
||||
'u' => true,
|
||||
'ul' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Array of explicitly disallowed tags
|
||||
*
|
||||
* IMPORTANT: Use lower-case names here because
|
||||
* of the case-insensitive matching
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $disallowedTags = [
|
||||
'iframe',
|
||||
'meta',
|
||||
'object',
|
||||
'script',
|
||||
'style',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of attributes that may contain URLs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $urlAttrs = [
|
||||
'href',
|
||||
'src',
|
||||
'xlink:href',
|
||||
];
|
||||
|
||||
/**
|
||||
* The document type (`'HTML'` or `'XML'`)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $type = 'HTML';
|
||||
|
||||
/**
|
||||
* Returns the sanitization options for the handler
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function options(): array
|
||||
{
|
||||
return array_merge(parent::options(), [
|
||||
'allowedAttrPrefixes' => static::$allowedAttrPrefixes,
|
||||
'allowedAttrs' => static::$allowedAttrs,
|
||||
'allowedNamespaces' => [],
|
||||
'allowedPIs' => [],
|
||||
'allowedTags' => static::$allowedTags,
|
||||
'disallowedTags' => static::$disallowedTags,
|
||||
'urlAttrs' => static::$urlAttrs,
|
||||
]);
|
||||
}
|
||||
}
|
209
kirby/src/Sane/Sane.php
Normal file
209
kirby/src/Sane/Sane.php
Normal file
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\F;
|
||||
|
||||
/**
|
||||
* The `Sane` class validates that files
|
||||
* don't contain potentially harmful contents.
|
||||
* The class comes with handlers for `svg`, `svgz` and `xml`
|
||||
* files for now, but can be extended and customized.
|
||||
* @since 3.5.4
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Sane
|
||||
{
|
||||
/**
|
||||
* Handler Type Aliases
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $aliases = [
|
||||
'application/xml' => 'xml',
|
||||
'image/svg' => 'svg',
|
||||
'image/svg+xml' => 'svg',
|
||||
'text/html' => 'html',
|
||||
'text/xml' => 'xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* All registered handlers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $handlers = [
|
||||
'html' => 'Kirby\Sane\Html',
|
||||
'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 . '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given string with the specified handler
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize(string $string, string $type): string
|
||||
{
|
||||
return static::handler($type)->sanitize($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the contents of a file by overwriting
|
||||
* the file with the sanitized version;
|
||||
* the sane handlers are automatically chosen by
|
||||
* the extension and MIME type if not specified
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @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\LogicException If more than one handler applies
|
||||
* @throws \Kirby\Exception\NotFoundException If the handler was not found
|
||||
* @throws \Kirby\Exception\Exception On other errors
|
||||
*/
|
||||
public static function sanitizeFile(string $file, $typeLazy = false): void
|
||||
{
|
||||
if (is_string($typeLazy) === true) {
|
||||
static::handler($typeLazy)->sanitizeFile($file);
|
||||
return;
|
||||
}
|
||||
|
||||
// try to find exactly one matching handler
|
||||
$handlers = static::handlersForFile($file, $typeLazy === true);
|
||||
switch (count($handlers)) {
|
||||
case 0:
|
||||
// lazy autodetection didn't find a handler
|
||||
break;
|
||||
case 1:
|
||||
$handlers[0]->sanitizeFile($file);
|
||||
break;
|
||||
default:
|
||||
// more than one matching handler;
|
||||
// sanitizing with all handlers will not leave much in the output
|
||||
$handlerNames = array_map('get_class', $handlers);
|
||||
throw new LogicException(
|
||||
'Cannot sanitize file as more than one handler applies: ' .
|
||||
implode(', ', $handlerNames)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file contents with the specified handler
|
||||
*
|
||||
* @param string $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;
|
||||
}
|
||||
|
||||
foreach (static::handlersForFile($file, $typeLazy === true) as $handler) {
|
||||
$handler->validateFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all handler objects that apply to the given file based on
|
||||
* file extension and MIME type
|
||||
*
|
||||
* @param string $file
|
||||
* @param bool $lazy If set to `true`, undefined handlers are skipped
|
||||
* @return array<\Kirby\Sane\Handler>
|
||||
*/
|
||||
protected static function handlersForFile(string $file, bool $lazy = false): array
|
||||
{
|
||||
$handlers = $handlerClasses = [];
|
||||
|
||||
// all values that can be used for the handler search;
|
||||
// filter out all empty options
|
||||
$options = array_filter([F::extension($file), F::mime($file)]);
|
||||
|
||||
foreach ($options as $option) {
|
||||
$handler = static::handler($option, $lazy);
|
||||
$handlerClass = $handler ? get_class($handler) : null;
|
||||
|
||||
// ensure that each handler class is only returned once
|
||||
if ($handler && in_array($handlerClass, $handlerClasses) === false) {
|
||||
$handlers[] = $handler;
|
||||
$handlerClasses[] = $handlerClass;
|
||||
}
|
||||
}
|
||||
|
||||
return $handlers;
|
||||
}
|
||||
}
|
509
kirby/src/Sane/Svg.php
Normal file
509
kirby/src/Sane/Svg.php
Normal file
|
@ -0,0 +1,509 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use DOMAttr;
|
||||
use DOMDocumentType;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Dom;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Sane handler for SVG files
|
||||
* @since 3.5.4
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Lukas Bestle <lukas@getkirby.com>
|
||||
* @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 <use> 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 <use> 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 <style> elements
|
||||
if ($element->tagName === 'style') {
|
||||
foreach (Dom::extractUrls($element->textContent) as $url) {
|
||||
if (Dom::isAllowedUrl($url, static::options()) !== true) {
|
||||
$errors[] = new InvalidArgumentException(
|
||||
'The URL is not allowed in the "style" element' .
|
||||
' (around line ' . $element->getLineNo() . ')'
|
||||
);
|
||||
Dom::remove($element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom callback for additional doctype validation
|
||||
* @internal
|
||||
*
|
||||
* @param \DOMDocumentType $doctype
|
||||
* @return void
|
||||
*/
|
||||
public static function validateDoctype(DOMDocumentType $doctype): void
|
||||
{
|
||||
if (mb_strtolower($doctype->name) !== 'svg') {
|
||||
throw new InvalidArgumentException('Invalid doctype');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sanitization options for the handler
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function options(): array
|
||||
{
|
||||
return array_merge(parent::options(), [
|
||||
'allowedAttrPrefixes' => static::$allowedAttrPrefixes,
|
||||
'allowedAttrs' => static::$allowedAttrs,
|
||||
'allowedNamespaces' => static::$allowedNamespaces,
|
||||
'allowedTags' => static::$allowedTags,
|
||||
'disallowedTags' => static::$disallowedTags,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given string into a `Toolkit\Dom` object
|
||||
*
|
||||
* @param string $string
|
||||
* @return \Kirby\Toolkit\Dom
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||
*/
|
||||
protected static function parse(string $string)
|
||||
{
|
||||
$svg = parent::parse($string);
|
||||
|
||||
// basic validation before we continue sanitizing/validating
|
||||
$rootName = $svg->document()->documentElement->nodeName;
|
||||
if ($rootName !== 'svg') {
|
||||
throw new InvalidArgumentException('The file is not a SVG (got <' . $rootName . '>)');
|
||||
}
|
||||
|
||||
return $svg;
|
||||
}
|
||||
}
|
72
kirby/src/Sane/Svgz.php
Normal file
72
kirby/src/Sane/Svgz.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Sane handler for gzip-compressed SVGZ files
|
||||
* @since 3.5.4
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Svgz extends Svg
|
||||
{
|
||||
/**
|
||||
* Sanitizes the given string
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed or recompressed
|
||||
*/
|
||||
public static function sanitize(string $string): string
|
||||
{
|
||||
$string = static::uncompress($string);
|
||||
$string = parent::sanitize($string);
|
||||
$string = @gzencode($string);
|
||||
|
||||
if (is_string($string) !== true) {
|
||||
throw new InvalidArgumentException('Could not recompress gzip data'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file contents
|
||||
*
|
||||
* @param string $string
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||
*/
|
||||
public static function validate(string $string): void
|
||||
{
|
||||
parent::validate(static::uncompress($string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncompresses the SVGZ data
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected static function uncompress(string $string): string
|
||||
{
|
||||
// only support uncompressed files up to 10 MB to
|
||||
// prevent gzip bombs from crashing the process
|
||||
$string = @gzdecode($string, 10000000);
|
||||
|
||||
if (is_string($string) !== true) {
|
||||
throw new InvalidArgumentException('Could not uncompress gzip data');
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
74
kirby/src/Sane/Xml.php
Normal file
74
kirby/src/Sane/Xml.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Sane;
|
||||
|
||||
use DOMDocumentType;
|
||||
use DOMElement;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Sane handler for XML files
|
||||
* @since 3.5.4
|
||||
*
|
||||
* @package Kirby Sane
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Xml extends DomHandler
|
||||
{
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
// if we are validating an XML file, block all SVG and HTML namespaces
|
||||
if (static::class === self::class) {
|
||||
$simpleXmlElement = simplexml_import_dom($element);
|
||||
foreach ($simpleXmlElement->getDocNamespaces(false, false) as $namespace => $value) {
|
||||
if (
|
||||
Str::contains($value, 'html', true) === true ||
|
||||
Str::contains($value, 'svg', true) === true
|
||||
) {
|
||||
$element->removeAttributeNS($value, $namespace);
|
||||
$errors[] = new InvalidArgumentException(
|
||||
'The namespace "' . $value . '" is not allowed' .
|
||||
' (around line ' . $element->getLineNo() . ')'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom callback for additional doctype validation
|
||||
* @internal
|
||||
*
|
||||
* @param \DOMDocumentType $doctype
|
||||
* @return void
|
||||
*/
|
||||
public static function validateDoctype(DOMDocumentType $doctype): void
|
||||
{
|
||||
// if we are validating an XML file, block all SVG and HTML doctypes
|
||||
if (
|
||||
static::class === self::class &&
|
||||
(
|
||||
Str::contains($doctype->name, 'html', true) === true ||
|
||||
Str::contains($doctype->name, 'svg', true) === true
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException('The doctype is not allowed in XML files');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue