Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
@@ -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,
],
];
}
}