diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 08d326ad8..97a2b0a4f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -25,6 +25,7 @@ class Handler extends ExceptionHandler protected $dontReport = [ NotFoundException::class, StoppedAuthenticationException::class, + NotifyException::class, ]; /** diff --git a/app/Exceptions/NotifyException.php b/app/Exceptions/NotifyException.php index b62b8fde6..d2fba30c4 100644 --- a/app/Exceptions/NotifyException.php +++ b/app/Exceptions/NotifyException.php @@ -6,18 +6,22 @@ use Exception; use Illuminate\Contracts\Support\Responsable; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +/** + * An exception that is thrown to notify the user of something which went wrong. + * Typically these should be translated messages since they will be shown to the end user + * via a pop up notification error message in the UI. + * + * This exception is not intended to be used for internal system/application errors, + * and therefore will not be logged by the exception handler. + */ class NotifyException extends Exception implements Responsable, HttpExceptionInterface { - public $message; - public string $redirectLocation; - protected int $status; - - public function __construct(string $message, string $redirectLocation = '/', int $status = 500) - { + public function __construct( + string $message, + public string $redirectLocation = '/', + protected int $status = 500 + ) { $this->message = $message; - $this->redirectLocation = $redirectLocation; - $this->status = $status; - parent::__construct(); } diff --git a/app/Search/SearchRunner.php b/app/Search/SearchRunner.php index 391254172..d49cb439a 100644 --- a/app/Search/SearchRunner.php +++ b/app/Search/SearchRunner.php @@ -290,7 +290,7 @@ class SearchRunner $query->where('name', '=', $tagParts['name']); } - if (is_numeric($tagParts['value']) && $tagParts['operator'] !== 'like') { + if (is_numeric($tagParts['value']) && is_finite($tagParts['value']) && $tagParts['operator'] !== 'like') { // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will // search the value as a string which prevents being able to do number-based operations // on the tag values. We ensure it has a numeric value and then cast it just to be sure. diff --git a/app/Uploads/Controllers/AttachmentController.php b/app/Uploads/Controllers/AttachmentController.php index 12f49dffe..bdd96d6ad 100644 --- a/app/Uploads/Controllers/AttachmentController.php +++ b/app/Uploads/Controllers/AttachmentController.php @@ -107,6 +107,9 @@ class AttachmentController extends Controller { /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); + $this->checkOwnablePermission(Permission::PageView, $attachment->page); + $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); + $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); try { $this->validate($request, [ @@ -120,10 +123,6 @@ class AttachmentController extends Controller ]), 422); } - $this->checkOwnablePermission(Permission::PageView, $attachment->page); - $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); - $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); - $attachment = $this->attachmentService->updateFile($attachment, [ 'name' => $request->input('attachment_edit_name'), 'link' => $request->input('attachment_edit_url'), @@ -142,6 +141,10 @@ class AttachmentController extends Controller public function attachLink(Request $request) { $pageId = $request->input('attachment_link_uploaded_to'); + $page = $this->pageQueries->findVisibleByIdOrFail($pageId); + + $this->checkPermission(Permission::AttachmentCreateAll); + $this->checkOwnablePermission(Permission::PageUpdate, $page); try { $this->validate($request, [ @@ -156,11 +159,6 @@ class AttachmentController extends Controller ]), 422); } - $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - - $this->checkPermission(Permission::AttachmentCreateAll); - $this->checkOwnablePermission(Permission::PageUpdate, $page); - $attachmentName = $request->input('attachment_link_name'); $link = $request->input('attachment_link_url'); $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId)); 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/composer.lock b/composer.lock index 710067eb0..e07268399 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.382.2", + "version": "3.384.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6844cc6421c47d6b96633ab8039045012acbeb27" + "reference": "c7d34f2d60515bd0c307e462268f75877842da4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6844cc6421c47d6b96633ab8039045012acbeb27", - "reference": "6844cc6421c47d6b96633ab8039045012acbeb27", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c7d34f2d60515bd0c307e462268f75877842da4a", + "reference": "c7d34f2d60515bd0c307e462268f75877842da4a", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.382.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.384.5" }, - "time": "2026-05-27T18:11:41+00:00" + "time": "2026-06-08T18:25:02+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1179,25 +1179,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.5", + "version": "7.11.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" + "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/5af96f374e0ab4ebd747b8310888c99d3adb0a8c", + "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.11", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1206,7 +1207,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzlehttp/test-server": "^0.5", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1286,7 +1287,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.5" + "source": "https://github.com/guzzle/guzzle/tree/7.11.1" }, "funding": [ { @@ -1302,24 +1303,25 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:53:46+00:00" + "time": "2026-06-07T22:54:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -1369,7 +1371,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.4.1" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1385,27 +1387,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:57:30+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.10.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130" + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7c1472269227dc6f18930bd903d7a88fe6c52130", - "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1486,7 +1490,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.10.3" + "source": "https://github.com/guzzle/psr7/tree/2.11.0" }, "funding": [ { @@ -1502,7 +1506,7 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:48:20+00:00" + "time": "2026-06-02T12:30:48+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1803,16 +1807,16 @@ }, { "name": "laravel/framework", - "version": "v12.61.0", + "version": "v12.61.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1124062a1ca92d290c8bcb9b7f649920fa6816bf" + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1124062a1ca92d290c8bcb9b7f649920fa6816bf", - "reference": "1124062a1ca92d290c8bcb9b7f649920fa6816bf", + "url": "https://api.github.com/repos/laravel/framework/zipball/e8472ca9774452fe50841d9bdced060679f4d58d", + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d", "shasum": "" }, "require": { @@ -2021,7 +2025,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-26T23:41:33+00:00" + "time": "2026-06-04T14:22:52+00:00" }, { "name": "laravel/prompts", @@ -4191,16 +4195,16 @@ }, { "name": "predis/predis", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "2033429520d8997a7815a2485f56abe6d2d0e075" + "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/2033429520d8997a7815a2485f56abe6d2d0e075", - "reference": "2033429520d8997a7815a2485f56abe6d2d0e075", + "url": "https://api.github.com/repos/predis/predis/zipball/8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", + "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", "shasum": "" }, "require": { @@ -4242,7 +4246,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v3.4.2" + "source": "https://github.com/predis/predis/tree/v3.5.0" }, "funding": [ { @@ -4250,7 +4254,7 @@ "type": "github" } ], - "time": "2026-03-09T20:33:04+00:00" + "time": "2026-06-02T19:25:56+00:00" }, { "name": "psr/clock", @@ -6845,16 +6849,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -6906,7 +6910,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -6926,7 +6930,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php80", @@ -7014,16 +7018,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "8339098cae28673c15cce00d80734af0453054e2" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", - "reference": "8339098cae28673c15cce00d80734af0453054e2", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -7070,7 +7074,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -7090,7 +7094,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", @@ -9179,11 +9183,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.0", + "version": "2.2.2", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4cd98348c809924f62bb9cc8c047f5e73bc9a58", - "reference": "b4cd98348c809924f62bb9cc8c047f5e73bc9a58", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", "shasum": "" }, "require": { @@ -9239,7 +9243,7 @@ "type": "github" } ], - "time": "2026-05-28T08:22:43+00:00" + "time": "2026-06-05T09:00:01+00:00" }, { "name": "phpunit/php-code-coverage", 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; '