From 81f77a95de4b060fa77a5376198a80a24cfcca59 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 6 Jun 2026 15:21:15 +0100 Subject: [PATCH] Content Filtering: Limited file protocol to just anchor hrefs Updated allow list/purifier system to only allow file protocol use on anchor hrefs to avoid potential security concerns with, after export, content being auto loaded via interactive elements like embeds/objects/videos etc... Updated tests to cover. Thanks to Gurmandeep Deol at Seneca Polytechnic for reporting. --- app/Util/HtmlContentFilter.php | 1 + .../ConfiguredHtmlPurifier.php | 19 ++++++- .../Filters/UriLimitFileProtocolToAnchors.php | 55 +++++++++++++++++++ tests/Entity/PageContentFilteringTest.php | 7 +++ 4 files changed, 79 insertions(+), 3 deletions(-) rename app/Util/{ => HtmlPurifier}/ConfiguredHtmlPurifier.php (86%) create mode 100644 app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php diff --git a/app/Util/HtmlContentFilter.php b/app/Util/HtmlContentFilter.php index 2ef797d13..26f22e6ca 100644 --- a/app/Util/HtmlContentFilter.php +++ b/app/Util/HtmlContentFilter.php @@ -2,6 +2,7 @@ namespace BookStack\Util; +use BookStack\Util\HtmlPurifier\ConfiguredHtmlPurifier; use DOMAttr; use DOMElement; use DOMNodeList; diff --git a/app/Util/ConfiguredHtmlPurifier.php b/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php similarity index 86% rename from app/Util/ConfiguredHtmlPurifier.php rename to app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php index 1f2528e71..307cdb74f 100644 --- a/app/Util/ConfiguredHtmlPurifier.php +++ b/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php @@ -1,13 +1,15 @@ getDefinition('HTML', true, true); if ($htmlDef instanceof HTMLPurifier_HTMLDefinition) { - $this->configureDefinition($htmlDef); + $this->configureHtmlDefinition($htmlDef); + } + + /** @var \HTMLPurifier_URIDefinition $uriDef */ + $uriDef = $config->getDefinition('URI', true, true); + if ($uriDef instanceof HTMLPurifier_URIDefinition) { + $this->configureUriDefinition($uriDef); } $this->purifier = new HTMLPurifier($config); @@ -91,7 +99,7 @@ class ConfiguredHtmlPurifier // $config->set('Cache.DefinitionImpl', null); // Disable cache during testing } - public function configureDefinition(HTMLPurifier_HTMLDefinition $definition): void + protected function configureHtmlDefinition(HTMLPurifier_HTMLDefinition $definition): void { // Allow the object element $definition->addElement( @@ -151,6 +159,11 @@ class ConfiguredHtmlPurifier $definition->addAttribute('a', 'data-mention-user-id', 'Number'); } + protected function configureUriDefinition(HTMLPurifier_URIDefinition $definition): void + { + $definition->registerFilter(new UriLimitFileProtocolToAnchors()); + } + public function purify(string $html): string { return $this->purifier->purify($html); diff --git a/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php b/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php new file mode 100644 index 000000000..19ca9cc82 --- /dev/null +++ b/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php @@ -0,0 +1,55 @@ +scheme !== 'file') { + return true; + } + + $token = $context->get('CurrentToken', true); + $attr = $context->get('CurrentAttr', true); + + // Only allow if used on hrefs on anchor tags + $isAnchor = $token && $token->name === 'a'; + $isHref = $attr === 'href'; + if ($isAnchor && $isHref) { + return true; + } + + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/tests/Entity/PageContentFilteringTest.php b/tests/Entity/PageContentFilteringTest.php index 449189a89..0ebbd9a0e 100644 --- a/tests/Entity/PageContentFilteringTest.php +++ b/tests/Entity/PageContentFilteringTest.php @@ -464,6 +464,11 @@ HTML; '
Hello!
' => '
Hello!
', '
Hello!
' => '
Hello!
', '
Hello!
' => '
Hello!
', + '' => '', + '' => '', + '' => '', + '
My local image
' => '
', + '
My local image
' => '
', ]; config()->set('app.content_filtering', 'a'); @@ -476,6 +481,7 @@ HTML; $resp = $this->get($page->getUrl()); $resp->assertSee($expected, false); + $resp->assertDontSee($input, false); } } @@ -484,6 +490,7 @@ HTML; $testCasesExpectedByInput = [ '

New tab linkydoodle

', '

@mentionusertext

', + '

Link to file

', '
Hello

Mydetailshere

', ];