Skip to content

[1.x] Merge pull request #1125 from phpDocumentor/feature/sanitize-raw-html #1127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 355 additions & 135 deletions composer.lock

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion packages/guides-cli/resources/schema/guides.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<xsd:element name="ignored_domain" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="inventory" type="inventory" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="template" type="template" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="raw-node" type="raw-node" minOccurs="0" maxOccurs="1" />
</xsd:choice>

<xsd:attribute name="input" type="xsd:string"/>
Expand All @@ -32,7 +33,6 @@
<xsd:attribute name="default-code-language" type="xsd:string"/>
<xsd:attribute name="links-are-relative" type="xsd:string"/>
<xsd:attribute name="max-menu-depth" type="xsd:int"/>

</xsd:complexType>

<xsd:complexType name="extension">
Expand Down Expand Up @@ -71,4 +71,24 @@
<xsd:attribute name="node" type="xsd:string" use="required"/>
<xsd:attribute name="format" type="xsd:string" default="html"/>
</xsd:complexType>

<xsd:complexType name="raw-node">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="sanitizer" type="sanitizer" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="escape" type="xsd:boolean" />
<xsd:attribute name="sanitizer-name" type="xsd:string" default="default" />
</xsd:complexType>

<xsd:complexType name="sanitizer">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="allow-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="block-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="drop-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="allow-attribute" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="drop-attribute" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>

<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:schema>
18 changes: 4 additions & 14 deletions packages/guides-markdown/src/Markdown/Parsers/HtmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,20 @@
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use Psr\Log\LoggerInterface;
use phpDocumentor\Guides\Nodes\RawNode;

use function assert;

/** @extends AbstractBlockParser<ParagraphNode> */
/** @extends AbstractBlockParser<RawNode> */
final class HtmlParser extends AbstractBlockParser
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}

public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): ParagraphNode
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): RawNode
{
assert($current instanceof HtmlBlock);

$this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML ');

$walker->next();

return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($current->getLiteral())])]);
return new RawNode($current->getLiteral());
}

public function supports(NodeWalkerEvent $event): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public function process(
BlockContext $blockContext,
Directive $directive,
): Node|null {
return new RawNode(implode("\n", $blockContext->getDocumentIterator()->toArray()));
return new RawNode(
implode("\n", $blockContext->getDocumentIterator()->toArray()),
$directive->getData(),
);
}
}
3 changes: 2 additions & 1 deletion packages/guides/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
"phpdocumentor/flyfinder": "^1.1",
"psr/event-dispatcher": "^1.0",
"symfony/clock": "^6.4.3",
"symfony/html-sanitizer": "^6.4.8",
"symfony/http-client": "^6.4.9",
"symfony/string": "^5.4 || ^6.3 || ^7.0",
"symfony/translation-contracts": "^3.4.1",
"symfony/http-client": "^6.4.4",
"twig/twig": "~2.15 || ^3.0",
"webmozart/assert": "^1.11"
},
Expand Down
10 changes: 9 additions & 1 deletion packages/guides/resources/config/guides.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use phpDocumentor\Guides\Compiler\NodeTransformers\CustomNodeTransformerFactory;
use phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers\InternalMenuEntryNodeTransformer;
use phpDocumentor\Guides\Compiler\NodeTransformers\NodeTransformerFactory;
use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer;
use phpDocumentor\Guides\Event\PostProjectNodeCreated;
use phpDocumentor\Guides\EventListener\LoadSettingsFromComposer;
use phpDocumentor\Guides\NodeRenderers\Html\BreadCrumbNodeRenderer;
Expand Down Expand Up @@ -62,6 +63,7 @@
use phpDocumentor\Guides\Twig\TwigTemplateRenderer;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Twig\Loader\FilesystemLoader;
Expand Down Expand Up @@ -105,6 +107,9 @@
->set(InternalMenuEntryNodeTransformer::class)
->tag('phpdoc.guides.compiler.nodeTransformers')

->set(RawNodeEscapeTransformer::class)
->arg('$escapeRawNodes', param('phpdoc.guides.raw_node.escape'))
->arg('$htmlSanitizerConfig', service('phpdoc.guides.raw_node.sanitizer.default'))

->set(AbsoluteUrlGenerator::class)
->set(RelativeUrlGenerator::class)
Expand Down Expand Up @@ -244,5 +249,8 @@
->arg('$themeManager', service(ThemeManager::class))

->set(TemplateRenderer::class, TwigTemplateRenderer::class)
->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class));
->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class))

->set('phpdoc.guides.raw_node.sanitizer.default', HtmlSanitizerConfig::class)
->call('allowSafeElements', [], true);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/

namespace phpDocumentor\Guides\Compiler\NodeTransformers;

use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use phpDocumentor\Guides\Nodes\RawNode;
use Psr\Log\LoggerInterface;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

use function assert;

/** @implements NodeTransformer<Node> */
final class RawNodeEscapeTransformer implements NodeTransformer
{
private HtmlSanitizer $htmlSanitizer;

public function __construct(
private readonly bool $escapeRawNodes,
private readonly LoggerInterface $logger,
HtmlSanitizerConfig $htmlSanitizerConfig,
) {
$this->htmlSanitizer = new HtmlSanitizer($htmlSanitizerConfig);
}

public function enterNode(Node $node, CompilerContext $compilerContext): Node
{
return $node;
}

public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
{
assert($node instanceof RawNode);
if ($this->escapeRawNodes) {
$this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML ');

return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($node->getValue())])]);
}

if ($node->getOption('format', 'html') === 'html') {
return new RawNode($this->htmlSanitizer->sanitize($node->getValue()));
}

return $node;
}

public function supports(Node $node): bool
{
return $node instanceof RawNode;
}

public function getPriority(): int
{
return 1000;
}
}
83 changes: 83 additions & 0 deletions packages/guides/src/DependencyInjection/GuidesExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace phpDocumentor\Guides\DependencyInjection;

use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer;
use phpDocumentor\Guides\DependencyInjection\Compiler\NodeRendererPass;
use phpDocumentor\Guides\DependencyInjection\Compiler\ParserRulesPass;
use phpDocumentor\Guides\DependencyInjection\Compiler\RendererPass;
Expand All @@ -31,6 +32,8 @@
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

use function array_keys;
use function array_map;
Expand Down Expand Up @@ -162,6 +165,33 @@ static function ($value) {
->end()
->end()
->end()
->arrayNode('raw_node')
->fixXmlConfig('sanitizer')
->children()
->booleanNode('escape')->defaultValue(false)->end()
->scalarNode('sanitizer_name')->end()
->arrayNode('sanitizers')
->defaultValue([])
->arrayPrototype()
->fixXmlConfig('allow_element')
->fixXmlConfig('drop_element')
->fixXmlConfig('block_element')
->fixXmlConfig('allow_attribute')
->fixXmlConfig('drop_attribute')
->children()
->scalarNode('name')->isRequired()->end()
->booleanNode('allow_safe_elements')->defaultValue(true)->end()
->booleanNode('allow_static_elements')->defaultValue(true)->end()
->arrayNode('allow_elements')->scalarPrototype()->end()->end()
->arrayNode('block_elements')->scalarPrototype()->end()->end()
->arrayNode('drop_elements')->scalarPrototype()->end()->end()
->arrayNode('allow_attributes')->scalarPrototype()->end()->end()
->arrayNode('drop_attributes')->scalarPrototype()->end()->end()
->end()
->end()
->end()
->end()
->end()
->scalarNode('default_code_language')->defaultValue('')->end()
->arrayNode('themes')
->defaultValue([])
Expand Down Expand Up @@ -290,6 +320,11 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('phpdoc.guides.base_template_paths', $config['base_template_paths']);
$container->setParameter('phpdoc.guides.node_templates', $config['templates']);
$container->setParameter('phpdoc.guides.inventories', $config['inventories']);
$container->setParameter('phpdoc.guides.raw_node.escape', $config['raw_node']['escape'] ?? false);

if ($config['raw_node'] ?? false) {
$this->configureSanitizers($config['raw_node'], $container);
}

foreach ($config['themes'] as $themeName => $themeConfig) {
$container->getDefinition(ThemeManager::class)
Expand Down Expand Up @@ -328,6 +363,54 @@ public function prepend(ContainerBuilder $container): void
],
);
}

/** @param array<string, mixed> $rawNodeConfig */
private function configureSanitizers(array $rawNodeConfig, ContainerBuilder $container): void
{
if ($rawNodeConfig['sanitizer_name'] ?? false) {
$container->getDefinition(RawNodeEscapeTransformer::class)
->setArgument('$htmlSanitizerConfig', new Reference('phpdoc.guides.raw_node.sanitizer.' . $rawNodeConfig['sanitizer_name']));
}

if (!is_array($rawNodeConfig['sanitizers'] ?? false)) {
return;
}

foreach ($rawNodeConfig['sanitizers'] as $sanitizerConfig) {
$def = $container->register('phpdoc.guides.raw_node.sanitizer.' . $sanitizerConfig['name'], HtmlSanitizerConfig::class);

// Base
if ($sanitizerConfig['allow_safe_elements']) {
$def->addMethodCall('allowSafeElements', [], true);
}

if ($sanitizerConfig['allow_static_elements']) {
$def->addMethodCall('allowStaticElements', [], true);
}

// Configures elements
foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) {
$def->addMethodCall('allowElement', [$element, $attributes], true);
}

foreach ($sanitizerConfig['block_elements'] as $element) {
$def->addMethodCall('blockElement', [$element], true);
}

foreach ($sanitizerConfig['drop_elements'] as $element) {
$def->addMethodCall('dropElement', [$element], true);
}

// Configures attributes
foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) {
$def->addMethodCall('allowAttribute', [$attribute, $elements], true);
}

foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) {
$def->addMethodCall('dropAttribute', [$attribute, $elements], true);
}
}
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/guides/src/Nodes/RawNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@

final class RawNode extends TextNode
{
public function __construct(string $contents, string $format = 'html')
{
parent::__construct($contents);

$this->options['format'] = $format;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- content start -->
<div class="section" id="directive-tests">
<h1>Directive tests</h1>
<p>Lorem Ipsum Dolor</p>
<p>Dolor sit!</p>
</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<guides xmlns="https://www.phpdoc.org/guides"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
input-format="rst"
>
<project title="Project Title" version="6.4"/>
<raw-node sanitizer-name="custom" >
<sanitizer name="custom">
<allow-element>p</allow-element>
<block-element>div</block-element>
<drop-element>h2</drop-element>
</sanitizer>
</raw-node>
</guides>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Directive tests
===============

.. raw:: html

<div class="someClass">
<script>alert('XSS');</script>
<p>Lorem Ipsum Dolor</p>
<h2>Some Rubric</h2>
<p>Dolor sit!</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- content start -->
<div class="section" id="directive-tests">
<h1>Directive tests</h1>
<div>
<p>Lorem Ipsum Dolor</p>
<h2>Some Rubric</h2>
<p>Dolor sit!</p>
</div>
</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<guides xmlns="https://www.phpdoc.org/guides"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
input-format="rst"
>
<project title="Project Title" version="6.4"/>
</guides>
Loading
Loading