mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Filtered scripts in custom HTML head for exports
Since it appeared to cause problems in some scenarios. Related to #2490
This commit is contained in:
parent
c50ac022a8
commit
43b6633183
@ -4,6 +4,7 @@ use BookStack\Entities\Models\Page;
|
|||||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMNodeList;
|
use DOMNodeList;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
@ -169,7 +170,7 @@ class PageContent
|
|||||||
$content = $this->page->html;
|
$content = $this->page->html;
|
||||||
|
|
||||||
if (!config('app.allow_content_scripts')) {
|
if (!config('app.allow_content_scripts')) {
|
||||||
$content = $this->escapeScripts($content);
|
$content = HtmlContentFilter::removeScripts($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($blankIncludes) {
|
if ($blankIncludes) {
|
||||||
@ -308,65 +309,4 @@ class PageContent
|
|||||||
|
|
||||||
return $innerContent;
|
return $innerContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape script tags within HTML content.
|
|
||||||
*/
|
|
||||||
protected function escapeScripts(string $html) : string
|
|
||||||
{
|
|
||||||
if (empty($html)) {
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument();
|
|
||||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
|
||||||
$xPath = new DOMXPath($doc);
|
|
||||||
|
|
||||||
// Remove standard script tags
|
|
||||||
$scriptElems = $xPath->query('//script');
|
|
||||||
foreach ($scriptElems as $scriptElem) {
|
|
||||||
$scriptElem->parentNode->removeChild($scriptElem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove clickable links to JavaScript URI
|
|
||||||
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
|
||||||
foreach ($badLinks as $badLink) {
|
|
||||||
$badLink->parentNode->removeChild($badLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove forms with calls to JavaScript URI
|
|
||||||
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
|
||||||
foreach ($badForms as $badForm) {
|
|
||||||
$badForm->parentNode->removeChild($badForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove meta tag to prevent external redirects
|
|
||||||
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
|
||||||
foreach ($metaTags as $metaTag) {
|
|
||||||
$metaTag->parentNode->removeChild($metaTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove data or JavaScript iFrames
|
|
||||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
|
||||||
foreach ($badIframes as $badIframe) {
|
|
||||||
$badIframe->parentNode->removeChild($badIframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 'on*' attributes
|
|
||||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
|
||||||
foreach ($onAttributes as $attr) {
|
|
||||||
/** @var \DOMAttr $attr*/
|
|
||||||
$attrName = $attr->nodeName;
|
|
||||||
$attr->parentNode->removeAttribute($attrName);
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = '';
|
|
||||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
|
||||||
foreach ($topElems as $child) {
|
|
||||||
$html .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ class HomeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function customHeadContent()
|
public function customHeadContent()
|
||||||
{
|
{
|
||||||
return view('partials.custom-head-content');
|
return view('partials.custom-head');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
71
app/Util/HtmlContentFilter.php
Normal file
71
app/Util/HtmlContentFilter.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php namespace BookStack\Util;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMNode;
|
||||||
|
use DOMNodeList;
|
||||||
|
use DOMXPath;
|
||||||
|
|
||||||
|
class HtmlContentFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Remove all of the script elements from the given HTML.
|
||||||
|
*/
|
||||||
|
public static function removeScripts(string $html): string
|
||||||
|
{
|
||||||
|
if (empty($html)) {
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xPath = new DOMXPath($doc);
|
||||||
|
|
||||||
|
// Remove standard script tags
|
||||||
|
$scriptElems = $xPath->query('//script');
|
||||||
|
static::removeNodes($scriptElems);
|
||||||
|
|
||||||
|
// Remove clickable links to JavaScript URI
|
||||||
|
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||||
|
static::removeNodes($badLinks);
|
||||||
|
|
||||||
|
// Remove forms with calls to JavaScript URI
|
||||||
|
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||||
|
static::removeNodes($badForms);
|
||||||
|
|
||||||
|
// Remove meta tag to prevent external redirects
|
||||||
|
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||||
|
static::removeNodes($metaTags);
|
||||||
|
|
||||||
|
// Remove data or JavaScript iFrames
|
||||||
|
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||||
|
static::removeNodes($badIframes);
|
||||||
|
|
||||||
|
// Remove 'on*' attributes
|
||||||
|
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
||||||
|
foreach ($onAttributes as $attr) {
|
||||||
|
/** @var \DOMAttr $attr*/
|
||||||
|
$attrName = $attr->nodeName;
|
||||||
|
$attr->parentNode->removeAttribute($attrName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = '';
|
||||||
|
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||||
|
foreach ($topElems as $child) {
|
||||||
|
$html .= $doc->saveHTML($child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removed all of the given DOMNodes.
|
||||||
|
*/
|
||||||
|
static protected function removeNodes(DOMNodeList $nodes): void
|
||||||
|
{
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$node->parentNode->removeChild($node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@yield('head')
|
@yield('head')
|
||||||
@include('partials.custom-head')
|
@include('partials.export-custom-head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@include('partials.custom-head')
|
@include('partials.export-custom-head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
</style>
|
</style>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@include('partials.custom-head')
|
@include('partials.export-custom-head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
@if(setting('app-custom-head', false))
|
|
||||||
<!-- Custom user content -->
|
|
||||||
{!! setting('app-custom-head') !!}
|
|
||||||
<!-- End custom user content -->
|
|
||||||
@endif
|
|
@ -1,5 +1,5 @@
|
|||||||
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
|
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
|
||||||
<!-- Custom user content -->
|
<!-- Custom user content -->
|
||||||
{!! setting('app-custom-head') !!}
|
{!! setting('app-custom-head') !!}
|
||||||
<!-- End custom user content -->
|
<!-- End custom user content -->
|
||||||
@endif
|
@endif
|
5
resources/views/partials/export-custom-head.blade.php
Normal file
5
resources/views/partials/export-custom-head.blade.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@if(setting('app-custom-head'))
|
||||||
|
<!-- Custom user content -->
|
||||||
|
{!! \BookStack\Util\HtmlContentFilter::removeScripts(setting('app-custom-head')) !!}
|
||||||
|
<!-- End custom user content -->
|
||||||
|
@endif
|
@ -1,5 +1,6 @@
|
|||||||
<?php namespace Tests\Entity;
|
<?php namespace Tests\Entity;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@ -214,4 +215,19 @@ class ExportTest extends TestCase
|
|||||||
$resp->assertSee('src="/uploads/svg_test.svg"');
|
$resp->assertSee('src="/uploads/svg_test.svg"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_exports_removes_scripts_from_custom_head()
|
||||||
|
{
|
||||||
|
$entities = [
|
||||||
|
Page::query()->first(), Chapter::query()->first(), Book::query()->first(),
|
||||||
|
];
|
||||||
|
setting()->put('app-custom-head', '<script>window.donkey = "cat";</script><style>.my-test-class { color: red; }</style>');
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$resp = $this->asEditor()->get($entity->getUrl('/export/html'));
|
||||||
|
$resp->assertDontSee('window.donkey');
|
||||||
|
$resp->assertDontSee('script');
|
||||||
|
$resp->assertSee('.my-test-class { color: red; }');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user