fix(security): add CSRF protection to all POST endpoints + contract tests (B2)

CSRF guards added to 16 admin pages that previously accepted POST requests
without token verification (GR-SEC-001): departments, permissions, roles,
settings, tenants, users (create/edit/bulk/tokens), and lang switch.

New contract tests:
- PostEndpointCsrfContractTest: scans all pages/ for POST handlers and
  verifies checkCsrfToken is present (API/data endpoints exempt).
- DatabaseUpdatesContractTest: validates db/updates/ scripts follow naming
  convention and use idempotent SQL patterns (GR-CORE-010).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 15:40:07 +01:00
parent 6286ec4179
commit fd90065048
18 changed files with 302 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -46,6 +47,12 @@ if ($allowedTenantIds) {
$tenants = [];
}
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/departments/create', 'csrf_expired');
Router::redirect('admin/departments/create');
return;
}
if ($request->hasBody('description')) {
$selectedTenantId = $request->bodyInt('tenant_id');
$selectedTenantIds = $directoryScopeGateway->filterTenantIdsForUser([$selectedTenantId], $currentUserId);

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -93,6 +94,12 @@ if ($allowedTenantIds) {
$form['tenant_id'] = 0;
}
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/departments/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/departments/edit/{$uuid}");
return;
}
if ($request->hasBody('description')) {
$submitDecision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT, [
'actor_user_id' => $currentUserId,

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -29,6 +30,12 @@ $form = [
'is_system' => 0,
];
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/permissions/create', 'csrf_expired');
Router::redirect('admin/permissions/create');
return;
}
if ($request->hasBody('key')) {
$result = app(\MintyPHP\Service\Access\PermissionGateway::class)->createFromAdmin($request->bodyAll());
$form = $result['form'] ?? $form;

View File

@@ -5,6 +5,7 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -44,6 +45,12 @@ $userWriteRepository = app(\MintyPHP\Repository\User\UserWriteRepository::class)
$selectedRoleIds = $rolePermissionRepository->listRoleIdsByPermissionId($id);
$previousRoleIds = $selectedRoleIds;
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/permissions/edit/{$id}", 'csrf_expired');
Router::redirect("admin/permissions/edit/{$id}");
return;
}
if ($request->hasBody('key')) {
$submitDecision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT, [
'actor_user_id' => $currentUserId,

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -32,6 +33,12 @@ $rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionReposi
$permissions = $permissionRepository->listActive();
$selectedPermissionIds = [];
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/roles/create', 'csrf_expired');
Router::redirect('admin/roles/create');
return;
}
if ($request->hasBody('description')) {
$dbSession = app(\MintyPHP\Repository\Support\DatabaseSessionRepository::class);
$transactionStarted = false;

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -63,6 +64,12 @@ $form = $role;
$permissions = $permissionRepository->listActive();
$selectedPermissionIds = $rolePermissionRepository->listPermissionIdsByRoleId($roleId);
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/roles/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/roles/edit/{$uuid}");
return;
}
if ($request->hasBody('description')) {
$submitDecision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_SUBMIT, [
'actor_user_id' => $currentUserId,

View File

@@ -5,6 +5,7 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
use MintyPHP\Service\Settings\AdminSettingsService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -33,6 +34,12 @@ $settings = is_array($pageData['settings'] ?? null) ? $pageData['settings'] : []
$activeLoginTokens = (int) ($pageData['active_login_tokens'] ?? 0);
$activeApiTokens = (int) ($pageData['active_api_tokens'] ?? 0);
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/settings', 'csrf_expired');
Router::redirect('admin/settings');
return;
}
if ($request->isMethod('POST')) {
$post = $request->bodyAll();
if (!array_key_exists('settings_submit', $post)) {

View File

@@ -4,6 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -52,6 +53,12 @@ $form = [
];
$ssoUiState = $canManageSso ? $tenantSsoService->buildMicrosoftUiState(0, $form) : [];
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/tenants/create', 'csrf_expired');
Router::redirect('admin/tenants/create');
return;
}
if ($request->hasBody('description')) {
$post = $request->bodyAll();
$ssoFields = [

View File

@@ -5,6 +5,7 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
use MintyPHP\Service\CustomField\TenantCustomFieldService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -95,6 +96,12 @@ $errorBag = formErrors();
$errors = [];
$form = array_merge($tenant, $ssoConfig);
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/tenants/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/tenants/edit/{$uuid}");
return;
}
if ($request->hasBody('description')) {
$post = $request->bodyAll();
$submitDecision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT, [

View File

@@ -4,6 +4,8 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\User\UserAccessPdfService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
$session = app(SessionStoreInterface::class)->all();
@@ -27,6 +29,12 @@ if (strtoupper((string) (requestInput()->method())) !== 'POST') {
return;
}
if (!Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/users', 'csrf_expired');
Router::redirect('admin/users');
return;
}
$errorBag = formErrors();
$raw = requestInput()->bodyAll()['uuids'] ?? '';
$uuids = [];

View File

@@ -3,6 +3,7 @@
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -31,6 +32,12 @@ if (!$decision->isAllowed()) {
return;
}
if (!Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/users/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/users/edit/{$uuid}");
return;
}
$name = trim((string) (requestInput()->bodyAll()['token_name'] ?? ''));
if ($name === '') {

View File

@@ -3,6 +3,7 @@
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -29,6 +30,12 @@ if (!$decision->isAllowed()) {
return;
}
if (!Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/users/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/users/edit/{$uuid}");
return;
}
$tokenId = (int) (requestInput()->bodyAll()['token_id'] ?? 0);
if ($tokenId > 0) {

View File

@@ -4,11 +4,17 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\User\UserAccessTemplateService;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
$session = app(SessionStoreInterface::class)->all();
Guard::requireLogin();
if (!Session::checkCsrfToken()) {
Router::json(['ok' => false, 'error' => 'csrf_expired']);
return;
}
$action = strtolower(trim((string) ($action ?? '')));
$allowedActions = ['activate', 'deactivate', 'delete', 'send-access'];
if (!in_array($action, $allowedActions, true)) {

View File

@@ -9,6 +9,7 @@ use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -91,6 +92,12 @@ $form = [
'active' => 1,
];
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), 'admin/users/create', 'csrf_expired');
Router::redirect('admin/users/create');
return;
}
if ($request->hasBody('email')) {
$post = $request->bodyAll();
$input = $post;

View File

@@ -8,6 +8,7 @@ use MintyPHP\Repository\Auth\RememberTokenRepository;
use MintyPHP\Router;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
@@ -173,6 +174,12 @@ if ($canViewSecurityArtifacts) {
$canManageApiTokens = $canManageApiTokens && $canViewSecurityArtifacts;
$showApiTokens = $canViewSecurityArtifacts && (!empty($apiTokens) || $canManageApiTokens);
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), "admin/users/edit/{$uuid}", 'csrf_expired');
Router::redirect("admin/users/edit/{$uuid}");
return;
}
if ($request->hasBody('email')) {
$post = $request->bodyAll();
$submitDecision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [

View File

@@ -5,12 +5,19 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\I18n;
use MintyPHP\Router;
use MintyPHP\Service\User\UserServicesFactory;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
$sessionStore = app(SessionStoreInterface::class);
$session = $sessionStore->all();
$request = requestInput();
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
Flash::error(t('Form expired, please try again'), '', 'csrf_expired');
Router::redirect('');
return;
}
$rawLocale = (string) $request->body(
'locale',
$request->query('locale', $request->query('lang', ''))