Skip to content

Commit 1077a4e

Browse files
committed
Merge branch 'v25-12' into development
2 parents 23f3f35 + 8020451 commit 1077a4e

25 files changed

+1218
-572
lines changed

.env.example.complete

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,25 @@ EXPORT_PDF_COMMAND_TIMEOUT=15
351351
# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
352352
WKHTMLTOPDF=false
353353

354-
# Allow <script> tags in page content
354+
# Allow JavaScript, and other potentiall dangerous content in page content.
355+
# This also removes CSP-level JavaScript control.
355356
# Note, if set to 'true' the page editor may still escape scripts.
357+
# DEPRECATED: Use 'APP_CONTENT_FILTERING' instead as detailed below. Activiting this option
358+
# effectively sets APP_CONTENT_FILTERING='' (No filtering)
356359
ALLOW_CONTENT_SCRIPTS=false
357360

361+
# Control the behaviour of content filtering, primarily used for page content.
362+
# This setting is a string of characters which represent different available filters:
363+
# - j - Filter out JavaScript and unknown binary data based content
364+
# - h - Filter out unexpected, and potentially dangerous, HTML elements
365+
# - f - Filter out unexpected form elements
366+
# - a - Run content through a more complex allowlist filter
367+
# This defaults to using all filters, unless ALLOW_CONTENT_SCRIPTS is set to true in which case no filters are used.
368+
# Note: These filters are a best-attempt and may not be 100% effective. They are typically a layer used in addition to other security measures.
369+
# Note: The default value will always be the most-strict, so it's advised to leave this unset in your own configuration
370+
# to ensure you are always using the full range of filters.
371+
APP_CONTENT_FILTERING="jfha"
372+
358373
# Indicate if robots/crawlers should crawl your instance.
359374
# Can be 'true', 'false' or 'null'.
360375
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.

app/Activity/Models/Comment.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use BookStack\Users\Models\HasCreatorAndUpdater;
99
use BookStack\Users\Models\OwnableInterface;
1010
use BookStack\Util\HtmlContentFilter;
11+
use BookStack\Util\HtmlContentFilterConfig;
1112
use Illuminate\Database\Eloquent\Builder;
1213
use Illuminate\Database\Eloquent\Factories\HasFactory;
1314
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -82,7 +83,8 @@ public function logDescriptor(): string
8283

8384
public function safeHtml(): string
8485
{
85-
return HtmlContentFilter::removeActiveContentFromHtmlString($this->html ?? '');
86+
$filter = new HtmlContentFilter(new HtmlContentFilterConfig());
87+
return $filter->filterString($this->html ?? '');
8688
}
8789

8890
public function jointPermissions(): HasMany

app/Config/app.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,24 @@
3737
// The limit for all uploaded files, including images and attachments in MB.
3838
'upload_limit' => env('FILE_UPLOAD_SIZE_LIMIT', 50),
3939

40-
// Allow <script> tags to entered within page content.
41-
// <script> tags are escaped by default.
42-
// Even when overridden the WYSIWYG editor may still escape script content.
43-
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
40+
// Control the behaviour of content filtering, primarily used for page content.
41+
// This setting is a string of characters which represent different available filters:
42+
// - j - Filter out JavaScript and unknown binary data based content
43+
// - h - Filter out unexpected, and potentially dangerous, HTML elements
44+
// - f - Filter out unexpected form elements
45+
// - a - Run content through a more complex allowlist filter
46+
// This defaults to using all filters, unless ALLOW_CONTENT_SCRIPTS is set to true in which case no filters are used.
47+
// Note: These filters are a best-attempt and may not be 100% effective. They are typically a layer used in addition to other security measures.
48+
'content_filtering' => env('APP_CONTENT_FILTERING', env('ALLOW_CONTENT_SCRIPTS', false) === true ? '' : 'jhfa'),
4449

4550
// Allow server-side fetches to be performed to potentially unknown
4651
// and user-provided locations. Primarily used in exports when loading
4752
// in externally referenced assets.
4853
'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
4954

5055
// Override the default behaviour for allowing crawlers to crawl the instance.
51-
// May be ignored if view has be overridden or modified.
52-
// Defaults to null since, if not set, 'app-public' status used instead.
56+
// May be ignored if the underlying view has been overridden or modified.
57+
// Defaults to null in which case the 'app-public' status is used instead.
5358
'allow_robots' => env('ALLOW_ROBOTS', null),
5459

5560
// Application Base URL, Used by laravel in development commands

app/Entities/Controllers/PageController.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use BookStack\Http\Controller;
2222
use BookStack\Permissions\Permission;
2323
use BookStack\References\ReferenceFetcher;
24+
use BookStack\Util\HtmlContentFilter;
25+
use BookStack\Util\HtmlContentFilterConfig;
2426
use Exception;
2527
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2628
use Illuminate\Http\Request;
@@ -173,7 +175,7 @@ public function show(string $bookSlug, string $pageSlug)
173175
}
174176

175177
/**
176-
* Get page from an ajax request.
178+
* Get a page from an ajax request.
177179
*
178180
* @throws NotFoundException
179181
*/
@@ -183,6 +185,10 @@ public function getPageAjax(int $pageId)
183185
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
184186
$page->makeHidden(['book']);
185187

188+
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
189+
$filter = new HtmlContentFilter($filterConfig);
190+
$page->html = $filter->filterString($page->html);
191+
186192
return response()->json($page);
187193
}
188194

app/Entities/Tools/EntityHtmlDescription.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use BookStack\Entities\Models\Bookshelf;
77
use BookStack\Entities\Models\Chapter;
88
use BookStack\Util\HtmlContentFilter;
9+
use BookStack\Util\HtmlContentFilterConfig;
910

1011
class EntityHtmlDescription
1112
{
@@ -55,7 +56,8 @@ public function getHtml(bool $raw = false): string
5556
return '<p></p>';
5657
}
5758

58-
return HtmlContentFilter::removeActiveContentFromHtmlString($html);
59+
$filter = new HtmlContentFilter(new HtmlContentFilterConfig());
60+
return $filter->filterString($html);
5961
}
6062

6163
public function getPlain(): string

app/Entities/Tools/PageContent.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace BookStack\Entities\Tools;
44

5+
use BookStack\App\AppVersion;
56
use BookStack\Entities\Models\Page;
67
use BookStack\Entities\Queries\PageQueries;
78
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
@@ -13,6 +14,7 @@
1314
use BookStack\Uploads\ImageService;
1415
use BookStack\Users\Models\User;
1516
use BookStack\Util\HtmlContentFilter;
17+
use BookStack\Util\HtmlContentFilterConfig;
1618
use BookStack\Util\HtmlDocument;
1719
use BookStack\Util\WebSafeMimeSniffer;
1820
use Closure;
@@ -317,11 +319,30 @@ public function render(bool $blankIncludes = false): string
317319
$this->updateIdsRecursively($doc->getBody(), 0, $idMap, $changeMap);
318320
}
319321

320-
if (!config('app.allow_content_scripts')) {
321-
HtmlContentFilter::removeActiveContentFromDocument($doc);
322+
$cacheKey = $this->getContentCacheKey($doc->getBodyInnerHtml());
323+
$cached = cache()->get($cacheKey, null);
324+
if ($cached !== null) {
325+
return $cached;
322326
}
323327

324-
return $doc->getBodyInnerHtml();
328+
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
329+
$filter = new HtmlContentFilter($filterConfig);
330+
$filtered = $filter->filterDocument($doc);
331+
332+
$cacheTime = 86400 * 7; // 1 week
333+
cache()->put($cacheKey, $filtered, $cacheTime);
334+
335+
return $filtered;
336+
}
337+
338+
protected function getContentCacheKey(string $html): string
339+
{
340+
$contentHash = md5($html);
341+
$contentId = $this->page->id;
342+
$contentTime = $this->page->updated_at?->timestamp ?? time();
343+
$appVersion = AppVersion::get();
344+
$filterConfig = config('app.content_filtering') ?? '';
345+
return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}";
325346
}
326347

327348
/**

app/Entities/Tools/PageEditorData.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
99
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
1010
use BookStack\Permissions\Permission;
11+
use BookStack\Util\HtmlContentFilter;
12+
use BookStack\Util\HtmlContentFilterConfig;
1113

1214
class PageEditorData
1315
{
@@ -47,6 +49,7 @@ protected function build(): array
4749
$isDraftRevision = false;
4850
$this->warnings = [];
4951
$editActivity = new PageEditActivity($page);
52+
$lastEditorId = $page->updated_by ?? user()->id;
5053

5154
if ($editActivity->hasActiveEditing()) {
5255
$this->warnings[] = $editActivity->activeEditingMessage();
@@ -58,11 +61,20 @@ protected function build(): array
5861
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
5962
$isDraftRevision = true;
6063
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
64+
$lastEditorId = $userDraft->created_by;
6165
}
6266

67+
// Get editor type and handle changes
6368
$editorType = $this->getEditorType($page);
6469
$this->updateContentForEditor($page, $editorType);
6570

71+
// Filter HTML content if required
72+
if ($editorType->isHtmlBased() && !old('html') && $lastEditorId !== user()->id) {
73+
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
74+
$filter = new HtmlContentFilter($filterConfig);
75+
$page->html = $filter->filterString($page->html);
76+
}
77+
6678
return [
6779
'page' => $page,
6880
'book' => $page->book,

app/Theming/CustomHtmlHeadContentProvider.php

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,16 @@
44

55
use BookStack\Util\CspService;
66
use BookStack\Util\HtmlContentFilter;
7+
use BookStack\Util\HtmlContentFilterConfig;
78
use BookStack\Util\HtmlNonceApplicator;
89
use Illuminate\Contracts\Cache\Repository as Cache;
910

1011
class CustomHtmlHeadContentProvider
1112
{
12-
/**
13-
* @var CspService
14-
*/
15-
protected $cspService;
16-
17-
/**
18-
* @var Cache
19-
*/
20-
protected $cache;
21-
22-
public function __construct(CspService $cspService, Cache $cache)
23-
{
24-
$this->cspService = $cspService;
25-
$this->cache = $cache;
13+
public function __construct(
14+
protected CspService $cspService,
15+
protected Cache $cache
16+
) {
2617
}
2718

2819
/**
@@ -50,7 +41,8 @@ public function forExport(): string
5041
$hash = md5($content);
5142

5243
return $this->cache->remember('custom-head-export:' . $hash, 86400, function () use ($content) {
53-
return HtmlContentFilter::removeActiveContentFromHtmlString($content);
44+
$config = new HtmlContentFilterConfig(filterOutNonContentElements: false, useAllowListFilter: false);
45+
return (new HtmlContentFilter($config))->filterString($content);
5446
});
5547
}
5648

0 commit comments

Comments
 (0)