diff --git a/core/tests/Unit/Manager/ElementListCsrfTokenTest.php b/core/tests/Unit/Manager/ElementListCsrfTokenTest.php new file mode 100644 index 0000000000..d942658626 --- /dev/null +++ b/core/tests/Unit/Manager/ElementListCsrfTokenTest.php @@ -0,0 +1,62 @@ +makeUrl\(.*?)"/', $source, $matches); + + $item = new class { + use ManagerActions; + + public bool $exists = true; + public array $managerActionsMap; + + public function getKey(): int + { + return 42; + } + }; + $item->managerActionsMap = (new ReflectionClass($model))->getDefaultProperties()['managerActionsMap']; + $originalSession = $_SESSION ?? null; + $_SESSION = ['_token' => str_repeat('a', 40)]; + $compiler = new BladeCompiler(new Filesystem(), sys_get_temp_dir()); + $mutating = 0; + + try { + // The module name runs it for managers who can execute but cannot edit modules. + foreach (['actions.edit', 'actions.run'] as $action) { + foreach ($matches[1] as $fragment) { + ob_start(); + try { + eval('?>' . $compiler->compileString($fragment)); + $url = html_entity_decode(ob_get_contents(), ENT_QUOTES, 'UTF-8'); + } finally { + ob_end_clean(); + } + parse_str((string) parse_url($url, PHP_URL_QUERY), $query); + expect($query['id'])->toBe('42'); + + if ($query['a'] === (string) $item->managerActionsMap['id']['actions.edit']) { + expect($query)->not->toHaveKey('_token'); + continue; + } + + expect($query['_token'] ?? null)->toBe($_SESSION['_token']); + $mutating++; + } + } + expect($mutating)->toBe($expectedLinks); + } finally { + $_SESSION = $originalSession; + } +})->with([ + 'templates' => ['template', EvolutionCMS\Models\SiteTemplate::class, 4], + 'chunks' => ['chunk', EvolutionCMS\Models\SiteHtmlsnippet::class, 8], + 'snippets' => ['snippet', EvolutionCMS\Models\SiteSnippet::class, 8], + 'plugins' => ['plugin', EvolutionCMS\Models\SitePlugin::class, 8], + 'TVs' => ['tv', EvolutionCMS\Models\SiteTmplvar::class, 4], + 'modules' => ['module', EvolutionCMS\Models\SiteModule::class, 11], +]); diff --git a/core/tests/Unit/Security/ManagerCsrfMiddlewareTest.php b/core/tests/Unit/Security/ManagerCsrfMiddlewareTest.php index 2bb649bbac..aec557222b 100644 --- a/core/tests/Unit/Security/ManagerCsrfMiddlewareTest.php +++ b/core/tests/Unit/Security/ManagerCsrfMiddlewareTest.php @@ -195,6 +195,12 @@ function csrfPassed(Request $request): bool expect(csrfPassed(csrfRequest('GET', ['a' => 27, 'id' => 3, 'action' => 'delete'])))->toBeTrue(); }); +it('does not reach an element delete processor without the current session token', function (int $action) { + expect(csrfPassed(csrfRequest('GET', ['a' => $action, 'id' => 3])))->toBeFalse() + ->and(csrfPassed(csrfRequest('GET', ['a' => $action, 'id' => 3, '_token' => 'stale'])))->toBeFalse() + ->and(csrfPassed(csrfRequest('GET', ['a' => $action, 'id' => 3, '_token' => str_repeat('a', 40)])))->toBeTrue(); +})->with([21, 80, 25, 104, 303, 110]); + it('accepts destructive GET actions carrying the session token', function () { $request = csrfRequest('GET', ['a' => 112, 'id' => 3, '_token' => str_repeat('a', 40)]); diff --git a/manager/views/page/resources/elements/chunk.blade.php b/manager/views/page/resources/elements/chunk.blade.php index 3dd44da330..6cd5e1655f 100644 --- a/manager/views/page/resources/elements/chunk.blade.php +++ b/manager/views/page/resources/elements/chunk.blade.php @@ -42,10 +42,10 @@ onclick="actionDisableElement(this)" title="@if($item->disabled) {{ ManagerTheme::getLexicon('enable') }} @else {{ ManagerTheme::getLexicon('disable') }} @endif" data-disabled="{{ $item->disabled }}" - data-enable-href="{{ $item->makeUrl('actions.enable', false, ['disabled' => 0]) }}" + data-enable-href="{{ $item->makeUrl('actions.enable', false, ['disabled' => 0, '_token' => csrf_token()]) }}" data-enable-title="{{ ManagerTheme::getLexicon('enable') }}" data-enable-icon="{{ $_style['icon_enable'] }}" - data-disable-href="{{ $item->makeUrl('actions.disable', false, ['disabled' => 1]) }}" + data-disable-href="{{ $item->makeUrl('actions.disable', false, ['disabled' => 1, '_token' => csrf_token()]) }}" data-disable-title="{{ ManagerTheme::getLexicon('disable') }}" data-disable-icon="{{ $_style['icon_disable'] }}" > @@ -55,14 +55,14 @@ @endif @if(evolutionCMS()->hasPermission('new_chunk'))