Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer;
|
||||
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'HtmlMailSanitizer' => 'onInitHtmlMailSanitizer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return HtmlMailSanitizer
|
||||
*/
|
||||
public function onInitHtmlMailSanitizer(ContainerInterface $container)
|
||||
{
|
||||
/** @var \Application $app */
|
||||
$app = $container->get('LegacyApplication');
|
||||
$tempDir = realpath($app->erp->GetTMP()) . DIRECTORY_SEPARATOR . 'HtmlPurifier';
|
||||
if (!is_dir($tempDir)) {
|
||||
@mkdir($tempDir, 0777);
|
||||
}
|
||||
|
||||
// Administration > Grundeinstellungen > System > Sicherheit > "Externe URL im Ticketsystem nicht laden"
|
||||
$externeurlsblockieren = (int)$app->erp->Firmendaten('externeurlsblockieren');
|
||||
$disableExternal = $externeurlsblockieren === 1;
|
||||
$disableExternalResources = $externeurlsblockieren === 1;
|
||||
|
||||
$hostname = $_SERVER['HTTP_HOST']; // Hostname ohne http/https
|
||||
$redirectUrl = './index.php?module=welcome&action=redirect&url=%s';
|
||||
$moduleActionWhitelist = [
|
||||
['module' => 'dateien', 'action' => 'send'], // Benötigt für Ticketsystem zur Anzeige von Dateianhängen
|
||||
];
|
||||
|
||||
$config = new SanitizerConfig(
|
||||
$disableExternal,
|
||||
$disableExternalResources,
|
||||
$hostname,
|
||||
$redirectUrl,
|
||||
$moduleActionWhitelist
|
||||
);
|
||||
|
||||
$config->setTempDir($tempDir);
|
||||
|
||||
return new HtmlMailSanitizer($config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class InitialisationFailedException extends RuntimeException implements SanitizerExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Exception;
|
||||
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements SanitizerExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class InvalidUrlException extends RuntimeException implements SanitizerExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class SanitationFailedException extends RuntimeException implements SanitizerExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface SanitizerExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Helper;
|
||||
|
||||
use Xentral\Components\Sanitizer\Exception\InvalidArgumentException;
|
||||
|
||||
final class InternalUriWhitelistChecker
|
||||
{
|
||||
/** @var string $hostname */
|
||||
private $hostname;
|
||||
|
||||
/** @var array $hostnameParts */
|
||||
private $hostnameParts;
|
||||
|
||||
/** @var array $moduleActionWhitelist */
|
||||
private $moduleActionWhitelist;
|
||||
|
||||
/**
|
||||
* @example `$whitelist = [
|
||||
* ['module' => 'testinger', 'action' => 'example'],
|
||||
* ['module' => 'welcome', 'action' => 'redirect'],
|
||||
* ]`
|
||||
*
|
||||
* @param string $hostname Der eigene Hostname; nur Domain ohne http
|
||||
* @param array $moduleActionWhitelist Erlaubte Module-Action-Kombinationen
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($hostname, $moduleActionWhitelist = [])
|
||||
{
|
||||
if (empty($hostname)) {
|
||||
throw new InvalidArgumentException(sprintf('Hostname "%s" is invalid.', $hostname));
|
||||
}
|
||||
|
||||
$this->hostname = (string)$hostname;
|
||||
$this->hostnameParts = array_reverse(explode('.', $this->hostname));
|
||||
$this->moduleActionWhitelist = (array)$moduleActionWhitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $uriHostname
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isOwnHost($uriHostname = null)
|
||||
{
|
||||
if ($uriHostname === null) {
|
||||
return true; // Domain fehlt => Relative URL './index.php?module=...'
|
||||
}
|
||||
if ($uriHostname === $this->hostname) {
|
||||
return true; // Domain stimmt 1:1 überein
|
||||
}
|
||||
|
||||
$uriHostParts = array_reverse(explode('.', $uriHostname));
|
||||
foreach ($this->hostnameParts as $index => $hostnamePart) {
|
||||
if (!isset($uriHostParts[$index])) {
|
||||
return false;
|
||||
}
|
||||
if ($this->hostnameParts[$index] !== $uriHostParts[$index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Geprüfte Domain ist Subdomain von $this->hostname > OK
|
||||
}
|
||||
|
||||
/**
|
||||
* Achtung: Methode prüft nicht ob Domain übereinstimmt; nur in Kombination mit `$this->isOwnHost` verwenden!
|
||||
*
|
||||
* @param string|null $module
|
||||
* @param string|null $action
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAllowedAction($module = null, $action = null)
|
||||
{
|
||||
if ($module === null || $action === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->moduleActionWhitelist as $params) {
|
||||
if ($params['module'] === $module && $params['action'] === $action) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Helper;
|
||||
|
||||
final class UriDefinition
|
||||
{
|
||||
/** @var string|null $scheme */
|
||||
private $scheme;
|
||||
|
||||
/** @var string|null $username */
|
||||
private $username;
|
||||
|
||||
/** @var string|null $password */
|
||||
private $password;
|
||||
|
||||
/** @var string|null $host */
|
||||
private $host;
|
||||
|
||||
/** @var int|null $port */
|
||||
private $port;
|
||||
|
||||
/** @var string|null $path */
|
||||
private $path;
|
||||
|
||||
/** @var array $queryParams */
|
||||
private $queryParams = [];
|
||||
|
||||
/** @var string|null $fragment */
|
||||
private $fragment;
|
||||
|
||||
/**
|
||||
* @param string|null $scheme
|
||||
* @param string|null $username
|
||||
* @param string|null $password
|
||||
* @param string|null $host
|
||||
* @param int|null $port
|
||||
* @param string|null $path
|
||||
* @param array|null $queryParams
|
||||
* @param string|null $fragment
|
||||
*/
|
||||
public function __construct(
|
||||
$scheme = null,
|
||||
$username = null,
|
||||
$password = null,
|
||||
$host = null,
|
||||
$port = null,
|
||||
$path = null,
|
||||
$queryParams = null,
|
||||
$fragment = null
|
||||
) {
|
||||
if (!empty($scheme)) {
|
||||
$this->scheme = strtolower($scheme);
|
||||
}
|
||||
if (!empty($username)) {
|
||||
$this->username = (string)$username;
|
||||
}
|
||||
if (!empty($password)) {
|
||||
$this->password = (string)$password;
|
||||
}
|
||||
if (!empty($host)) {
|
||||
$this->host = (string)$host;
|
||||
}
|
||||
if (!empty($port)) {
|
||||
$this->port = (int)$port;
|
||||
}
|
||||
if (!empty($path)) {
|
||||
$this->path = (string)$path;
|
||||
}
|
||||
if (is_array($queryParams)) {
|
||||
$this->queryParams = $queryParams;
|
||||
}
|
||||
if (!empty($fragment)) {
|
||||
$this->fragment = (string)$fragment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getScheme()
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getQueryParam($name)
|
||||
{
|
||||
return isset($this->queryParams[$name]) ? $this->queryParams[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQueryParams()
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFragment()
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\Helper;
|
||||
|
||||
use Xentral\Components\Sanitizer\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Sanitizer\Exception\InvalidUrlException;
|
||||
|
||||
final class UriParser
|
||||
{
|
||||
/**
|
||||
* @param string $url
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws InvalidUrlException
|
||||
*
|
||||
* @return UriDefinition
|
||||
*/
|
||||
public function parse($url)
|
||||
{
|
||||
if (!is_string($url) || empty($url)) {
|
||||
throw new InvalidArgumentException('Url is invalid. Url can not be empty.');
|
||||
}
|
||||
|
||||
$parts = @parse_url($url);
|
||||
if ($parts === false) {
|
||||
throw new InvalidUrlException(sprintf('Could not parse url: "%s"', $url));
|
||||
}
|
||||
|
||||
$queryParams = [];
|
||||
if (isset($parts['query'])) {
|
||||
parse_str($parts['query'], $queryParams);
|
||||
}
|
||||
|
||||
return new UriDefinition(
|
||||
isset($parts['scheme']) ? $parts['scheme'] : null,
|
||||
isset($parts['user']) ? $parts['user'] : null,
|
||||
isset($parts['pass']) ? $parts['pass'] : null,
|
||||
isset($parts['host']) ? $parts['host'] : null,
|
||||
isset($parts['port']) ? (int)$parts['port'] : null,
|
||||
isset($parts['path']) ? $parts['path'] : null,
|
||||
is_array($queryParams) ? $queryParams : null,
|
||||
isset($parts['fragment']) ? $parts['fragment'] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer;
|
||||
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
use HTMLPurifier_Exception;
|
||||
use Xentral\Components\Sanitizer\Exception\InitialisationFailedException;
|
||||
use Xentral\Components\Sanitizer\Exception\SanitationFailedException;
|
||||
|
||||
final class HtmlMailSanitizer
|
||||
{
|
||||
/** @var HTMLPurifier $purifier */
|
||||
private $purifier;
|
||||
|
||||
/**
|
||||
* @param SanitizerConfig $config
|
||||
*
|
||||
* @throws InitialisationFailedException
|
||||
*/
|
||||
public function __construct(SanitizerConfig $config)
|
||||
{
|
||||
try {
|
||||
$purifierConf = HTMLPurifier_Config::create($config->toHtmlPurifierConfig());
|
||||
$this->purifier = new HTMLPurifier($purifierConf);
|
||||
} catch (HTMLPurifier_Exception $exception) {
|
||||
throw new InitialisationFailedException('Failed to initialize HtmlMailSanitizer', 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_Config
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->purifier->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mailContent
|
||||
* @param SanitizerConfig $config
|
||||
*
|
||||
* @throws SanitationFailedException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize($mailContent, SanitizerConfig $config = null)
|
||||
{
|
||||
$purifierConf = $config !== null ? HTMLPurifier_Config::create($config->toHtmlPurifierConfig()) : null;
|
||||
|
||||
try {
|
||||
$cleanContent = $this->purifier->purify($mailContent, $purifierConf);
|
||||
} catch (HTMLPurifier_Exception $exception) {
|
||||
throw new SanitationFailedException($exception->getMessage(), $exception->getCode(), $exception);
|
||||
}
|
||||
|
||||
return $cleanContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer\HtmlPurifier;
|
||||
|
||||
use HTMLPurifier_Injector;
|
||||
use HTMLPurifier_Token;
|
||||
use HTMLPurifier_Token_Empty;
|
||||
use HTMLPurifier_Token_Start;
|
||||
use Xentral\Components\Sanitizer\Exception\SanitizerExceptionInterface;
|
||||
use Xentral\Components\Sanitizer\Helper\InternalUriWhitelistChecker;
|
||||
use Xentral\Components\Sanitizer\Helper\UriParser;
|
||||
|
||||
class InternalUrlWhitelist extends HTMLPurifier_Injector
|
||||
{
|
||||
/** @var string $name */
|
||||
public $name = 'InternalUrlWhitelist';
|
||||
|
||||
/** @var array $needed */
|
||||
public $needed = ['a', 'img'];
|
||||
|
||||
/** @var InternalUriWhitelistChecker $checker */
|
||||
protected $checker;
|
||||
|
||||
/** @var UriParser $parser */
|
||||
protected $parser;
|
||||
|
||||
/**
|
||||
* @param UriParser $parser
|
||||
* @param InternalUriWhitelistChecker $checker
|
||||
*/
|
||||
public function __construct(UriParser $parser, InternalUriWhitelistChecker $checker)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->checker = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image-URLs verarbeiten
|
||||
*
|
||||
* @param HTMLPurifier_Token_Empty $token
|
||||
*/
|
||||
public function handleElement(&$token)
|
||||
{
|
||||
if ($token->name !== 'img' || !isset($token->attr['src'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$url = $token->attr['src'];
|
||||
$uri = $this->parser->parse($url);
|
||||
} catch (SanitizerExceptionInterface $exception) {
|
||||
unset($token->attr['src']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checker->isOwnHost($uri->getHost())) {
|
||||
$module = $uri->getQueryParam('module');
|
||||
$action = $uri->getQueryParam('action');
|
||||
if (!$this->checker->isAllowedAction($module, $action)) {
|
||||
unset($token->attr['src']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hyperlink-URLs verarbeiten
|
||||
*
|
||||
* @param HTMLPurifier_Token $token
|
||||
*/
|
||||
public function handleEnd(&$token)
|
||||
{
|
||||
/** @var HTMLPurifier_Token_Start $startToken */
|
||||
$startToken = $token->start;
|
||||
if ($startToken->name !== 'a' || !isset($startToken->attr['href'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$url = $startToken->attr['href'];
|
||||
$uri = $this->parser->parse($url);
|
||||
} catch (SanitizerExceptionInterface $exception) {
|
||||
unset($startToken->attr['href']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checker->isOwnHost($uri->getHost())) {
|
||||
$module = $uri->getQueryParam('module');
|
||||
$action = $uri->getQueryParam('action');
|
||||
if (!$this->checker->isAllowedAction($module, $action)) {
|
||||
unset($startToken->attr['href']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Sanitizer;
|
||||
|
||||
use Xentral\Components\Sanitizer\Helper\InternalUriWhitelistChecker;
|
||||
use Xentral\Components\Sanitizer\Helper\UriParser;
|
||||
use Xentral\Components\Sanitizer\HtmlPurifier\InternalUrlWhitelist;
|
||||
|
||||
final class SanitizerConfig
|
||||
{
|
||||
/** @var bool $disableExternal */
|
||||
private $disableExternal;
|
||||
|
||||
/** @var bool $disableExternalResources */
|
||||
private $disableExternalResources;
|
||||
|
||||
/** @var string|null $hostname */
|
||||
private $hostname;
|
||||
|
||||
/** @var string|null $redirectUrl */
|
||||
private $redirectUrl;
|
||||
|
||||
/** @var array $moduleActionWhitelist */
|
||||
private $moduleActionWhitelist;
|
||||
|
||||
/** @var string|null $tempDir */
|
||||
private $tempDir;
|
||||
|
||||
/**
|
||||
* @param bool $disableExternal Removes all external links and resources
|
||||
* @param bool $disableExternalResources Removes only external resources; external links are allowed
|
||||
* @param string|null $hostname Domain name of the server; without http/https
|
||||
* @param string|null $redirectUrl Munges all browsable (usually http, https and ftp) absolute URIs
|
||||
* into another URI; example `http://my-redirect-service.com/?url=%s`
|
||||
* @param array $moduleActionWhitelist Example `[ ['module' => 'welcome', 'action' => 'redirect'] ]`
|
||||
*/
|
||||
public function __construct(
|
||||
$disableExternal = false,
|
||||
$disableExternalResources = false,
|
||||
$hostname = null,
|
||||
$redirectUrl = null,
|
||||
$moduleActionWhitelist = []
|
||||
) {
|
||||
$this->disableExternal = (bool)$disableExternal;
|
||||
$this->disableExternalResources = (bool)$disableExternalResources;
|
||||
$this->moduleActionWhitelist = (array)$moduleActionWhitelist;
|
||||
if (is_string($hostname) && !empty($hostname)) {
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
if (is_string($redirectUrl) && !empty($redirectUrl)) {
|
||||
$this->redirectUrl = $redirectUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tempDir
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setTempDir($tempDir)
|
||||
{
|
||||
$this->tempDir = $tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toHtmlPurifierConfig()
|
||||
{
|
||||
$config = $this->getPurifierDefaults();
|
||||
$config['URI']['DisableExternal'] = $this->disableExternal;
|
||||
$config['URI']['DisableExternalResources'] = $this->disableExternalResources;
|
||||
|
||||
if ($this->redirectUrl !== null) {
|
||||
$config['URI']['Munge'] = $this->redirectUrl;
|
||||
}
|
||||
if ($this->hostname !== null) {
|
||||
$config['URI']['Host'] = $this->hostname;
|
||||
}
|
||||
if ($this->tempDir !== null && is_dir($this->tempDir)) {
|
||||
$config['Cache'] = [
|
||||
'DefinitionImpl' => 'Serializer',
|
||||
'SerializerPath' => $this->tempDir,
|
||||
];
|
||||
}
|
||||
|
||||
// Nur bestimmte interne URLs zulassen; alle anderen entfernen
|
||||
if (!empty($this->hostname) && !empty($this->moduleActionWhitelist)) {
|
||||
$checker = new InternalUriWhitelistChecker($this->hostname, $this->moduleActionWhitelist);
|
||||
$autoFormatter = new InternalUrlWhitelist(new UriParser(), $checker);
|
||||
|
||||
if (!isset($config['AutoFormat']['Custom'])) {
|
||||
$config['AutoFormat']['Custom'] = [];
|
||||
}
|
||||
$config['AutoFormat']['Custom'][] = $autoFormatter;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://htmlpurifier.org/live/configdoc/plain.html
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getPurifierDefaults()
|
||||
{
|
||||
return [
|
||||
'URI' => [
|
||||
'DisableExternal' => false,
|
||||
'DisableExternalResources' => false,
|
||||
'Munge' => null,
|
||||
'Host' => null,
|
||||
],
|
||||
'HTML' => [
|
||||
'TidyLevel' => 'medium',
|
||||
'TargetBlank' => true,
|
||||
'TargetNoopener' => true,
|
||||
'TargetNoreferrer' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user