Skip to content
Open
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
17 changes: 16 additions & 1 deletion .env.example.complete
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,25 @@ EXPORT_PDF_COMMAND_TIMEOUT=15
# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
WKHTMLTOPDF=false

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

# Control the behaviour of content filtering, primarily used for page content.
# This setting is a string of characters which represent different available filters:
# - j - Filter out JavaScript and unknown binary data based content
# - h - Filter out unexpected, and potentially dangerous, HTML elements
# - f - Filter out unexpected form elements
# - a - Run content through a more complex allowlist filter
# This defaults to using all filters, unless ALLOW_CONTENT_SCRIPTS is set to true in which case no filters are used.
# 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.
# Note: The default value will always be the most-strict, so it's advised to leave this unset in your own configuration
# to ensure you are always using the full range of filters.
APP_CONTENT_FILTERING="jfha"

# Indicate if robots/crawlers should crawl your instance.
# Can be 'true', 'false' or 'null'.
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.
Expand Down
4 changes: 3 additions & 1 deletion app/Activity/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand Down Expand Up @@ -82,7 +83,8 @@ public function logDescriptor(): string

public function safeHtml(): string
{
return HtmlContentFilter::removeActiveContentFromHtmlString($this->html ?? '');
$filter = new HtmlContentFilter(new HtmlContentFilterConfig());
return $filter->filterString($this->html ?? '');
}

public function jointPermissions(): HasMany
Expand Down
17 changes: 11 additions & 6 deletions app/Config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,24 @@
// The limit for all uploaded files, including images and attachments in MB.
'upload_limit' => env('FILE_UPLOAD_SIZE_LIMIT', 50),

// Allow <script> tags to entered within page content.
// <script> tags are escaped by default.
// Even when overridden the WYSIWYG editor may still escape script content.
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
// Control the behaviour of content filtering, primarily used for page content.
// This setting is a string of characters which represent different available filters:
// - j - Filter out JavaScript and unknown binary data based content
// - h - Filter out unexpected, and potentially dangerous, HTML elements
// - f - Filter out unexpected form elements
// - a - Run content through a more complex allowlist filter
// This defaults to using all filters, unless ALLOW_CONTENT_SCRIPTS is set to true in which case no filters are used.
// 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.
'content_filtering' => env('APP_CONTENT_FILTERING', env('ALLOW_CONTENT_SCRIPTS', false) === true ? '' : 'jhfa'),

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

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

// Application Base URL, Used by laravel in development commands
Expand Down
8 changes: 7 additions & 1 deletion app/Entities/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -173,7 +175,7 @@ public function show(string $bookSlug, string $pageSlug)
}

/**
* Get page from an ajax request.
* Get a page from an ajax request.
*
* @throws NotFoundException
*/
Expand All @@ -183,6 +185,10 @@ public function getPageAjax(int $pageId)
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->makeHidden(['book']);

$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
$filter = new HtmlContentFilter($filterConfig);
$page->html = $filter->filterString($page->html);

return response()->json($page);
}

Expand Down
4 changes: 3 additions & 1 deletion app/Entities/Tools/EntityHtmlDescription.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;

class EntityHtmlDescription
{
Expand Down Expand Up @@ -50,7 +51,8 @@ public function getHtml(bool $raw = false): string
return $html;
}

return HtmlContentFilter::removeActiveContentFromHtmlString($html);
$filter = new HtmlContentFilter(new HtmlContentFilterConfig());
return $filter->filterString($html);
}

public function getPlain(): string
Expand Down
27 changes: 24 additions & 3 deletions app/Entities/Tools/PageContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace BookStack\Entities\Tools;

use BookStack\App\AppVersion;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
Expand All @@ -13,6 +14,7 @@
use BookStack\Uploads\ImageService;
use BookStack\Users\Models\User;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;
use BookStack\Util\HtmlDocument;
use BookStack\Util\WebSafeMimeSniffer;
use Closure;
Expand Down Expand Up @@ -317,11 +319,30 @@ public function render(bool $blankIncludes = false): string
$this->updateIdsRecursively($doc->getBody(), 0, $idMap, $changeMap);
}

if (!config('app.allow_content_scripts')) {
HtmlContentFilter::removeActiveContentFromDocument($doc);
$cacheKey = $this->getContentCacheKey($doc->getBodyInnerHtml());
$cached = cache()->get($cacheKey, null);
if ($cached !== null) {
return $cached;
}

return $doc->getBodyInnerHtml();
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
$filter = new HtmlContentFilter($filterConfig);
$filtered = $filter->filterDocument($doc);

$cacheTime = 86400 * 7; // 1 week
cache()->put($cacheKey, $filtered, $cacheTime);

return $filtered;
}

protected function getContentCacheKey(string $html): string
{
$contentHash = md5($html);
$contentId = $this->page->id;
$contentTime = $this->page->updated_at?->timestamp ?? time();
$appVersion = AppVersion::get();
$filterConfig = config('app.content_filtering') ?? '';
return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}";
}

/**
Expand Down
12 changes: 12 additions & 0 deletions app/Entities/Tools/PageEditorData.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Permissions\Permission;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;

class PageEditorData
{
Expand Down Expand Up @@ -47,6 +49,7 @@ protected function build(): array
$isDraftRevision = false;
$this->warnings = [];
$editActivity = new PageEditActivity($page);
$lastEditorId = $page->updated_by ?? user()->id;

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

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

// Filter HTML content if required
if ($editorType->isHtmlBased() && !old('html') && $lastEditorId !== user()->id) {
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
$filter = new HtmlContentFilter($filterConfig);
$page->html = $filter->filterString($page->html);
}

return [
'page' => $page,
'book' => $page->book,
Expand Down
22 changes: 7 additions & 15 deletions app/Theming/CustomHtmlHeadContentProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,16 @@

use BookStack\Util\CspService;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig;
use BookStack\Util\HtmlNonceApplicator;
use Illuminate\Contracts\Cache\Repository as Cache;

class CustomHtmlHeadContentProvider
{
/**
* @var CspService
*/
protected $cspService;

/**
* @var Cache
*/
protected $cache;

public function __construct(CspService $cspService, Cache $cache)
{
$this->cspService = $cspService;
$this->cache = $cache;
public function __construct(
protected CspService $cspService,
protected Cache $cache
) {
}

/**
Expand Down Expand Up @@ -50,7 +41,8 @@ public function forExport(): string
$hash = md5($content);

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

Expand Down
Loading