Merge branch 'v26051_sec' into development

This commit is contained in:
Dan Brown
2026-06-09 12:48:39 +01:00
11 changed files with 206 additions and 79 deletions

View File

@@ -25,6 +25,7 @@ class Handler extends ExceptionHandler
protected $dontReport = [
NotFoundException::class,
StoppedAuthenticationException::class,
NotifyException::class,
];
/**

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -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));

View File

@@ -2,6 +2,7 @@
namespace BookStack\Util;
use BookStack\Util\HtmlPurifier\ConfiguredHtmlPurifier;
use DOMAttr;
use DOMElement;
use DOMNodeList;

View File

@@ -1,13 +1,15 @@
<?php
namespace BookStack\Util;
namespace BookStack\Util\HtmlPurifier;
use BookStack\App\AppVersion;
use BookStack\Util\HtmlPurifier\Filters\UriLimitFileProtocolToAnchors;
use HTMLPurifier;
use HTMLPurifier_Config;
use HTMLPurifier_DefinitionCache_Serializer;
use HTMLPurifier_HTML5Config;
use HTMLPurifier_HTMLDefinition;
use HTMLPurifier_URIDefinition;
/**
* Provides a configured HTML Purifier instance.
@@ -33,7 +35,13 @@ class ConfiguredHtmlPurifier
$htmlDef = $config->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);

View File

@@ -0,0 +1,55 @@
<?php
namespace BookStack\Util\HtmlPurifier\Filters;
use HTMLPurifier_Config;
use HTMLPurifier_Context;
use HTMLPurifier_URI;
use HTMLPurifier_URIFilter;
/**
* Limits file:// URIs to only be used on anchor tags href attributes.
* This prevents use on iframes/embeds/images where they can be used to load external
* content on the network, triggering calls which may include NTLM auth hashes when in
* certain windows based environments.
*/
class UriLimitFileProtocolToAnchors extends HTMLPurifier_URIFilter
{
/**
* @type string
*/
public $name = 'LimitFileProtocolToAnchors';
/**
* @type bool
*/
public $always_load = true;
/**
* @param HTMLPurifier_URI $uri
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool
*/
public function filter(&$uri, $config, $context)
{
// Ensure we're only filtering file:// URIs'
if ($uri->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

118
composer.lock generated
View File

@@ -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",

View File

@@ -464,6 +464,11 @@ HTML;
'<div style="background:#FF0000;left:0;color:#00FFEE;">Hello!</div>' => '<div style="background:#FF0000;color:#00FFEE;">Hello!</div>',
'<div style="color:#00FFEE;">Hello!<style>testinghello!</style></div>' => '<div style="color:#00FFEE;">Hello!</div>',
'<div drawio-diagram="5332" another-attr="cat">Hello!</div>' => '<div drawio-diagram="5332">Hello!</div>',
'<iframe src="file://link/to/file" id="bkmrk-file-iframe"></iframe>' => '<iframe id="bkmrk-file-iframe"></iframe>',
'<embed src="file://link/to/file" id="bkmrk-file-embed"></embed>' => '<embed id="bkmrk-file-embed">',
'<object data="file://link/to/file" id="bkmrk-file-object"></object>' => '<object id="bkmrk-file-object">',
'<div id="bkmrk-file-img"><img src="file://link/to/file" alt="My local image"></div>' => '<div id="bkmrk-file-img"></div>',
'<div id="bkmrk-file-img"><img srcset="file://link/to/file" alt="My local image"></div>' => '<div id="bkmrk-file-img"></div>',
];
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 = [
'<p><a href="https://example.com" target="_blank">New tab linkydoodle</a></p>',
'<p><a href="https://example.com/user/1" data-mention-user-id="5">@mentionusertext</a></p>',
'<p><a href="file://link/to/file">Link to file</a></p>',
'<details><summary>Hello</summary><p>Mydetailshere</p></details>',
];

View File

@@ -233,6 +233,18 @@ class EntitySearchTest extends TestCase
$this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
}
public function test_search_tags_with_unexpected_numeric_values_does_not_cause_error()
{
$pageA = $this->entities->page();
$pageA->name = 'MyTestPageWithAwkwardNumericTagValue';
$pageA->save();
$pageA->tags()->save(new Tag(['name' => 'Count', 'value' => '1E999']));
$resp = $this->asEditor()->get('/search?term=' . urlencode('[Count=1E999]'));
$resp->assertStatus(200);
$resp->assertSee('MyTestPageWithAwkwardNumericTagValue');
}
public function test_entity_selector_search()
{
$page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);

View File

@@ -159,6 +159,38 @@ class AttachmentTest extends TestCase
$this->files->deleteAllAttachmentFiles();
}
public function test_attachment_update_without_permission()
{
$page = $this->entities->page();
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
$this->permissions->disableEntityInheritedPermissions($page);
$resp = $this->asViewer()->put("attachments/{$attachment->id}", [
'attachment_edit_name' => 'My new attachment name',
'attachment_edit_url' => 'https://test.example.com',
]);
$this->assertPermissionError($resp);
}
public function test_attachment_update_without_permission_with_validation_errors()
{
$page = $this->entities->page();
/** @var Attachment $attachment */
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
$this->permissions->disableEntityInheritedPermissions($page);
$resp = $this->asViewer()->put("attachments/{$attachment->id}", [
'attachment_edit_name' => '',
'attachment_edit_url' => 'https://test.example.com',
]);
$this->assertPermissionError($resp);
$resp->assertDontSee($attachment->path);
}
public function test_file_deletion()
{
$page = $this->entities->page();