Skip to content

Access Control Patterns

When to Use

Every AJAX callback and route is an HTTP endpoint and requires access control. AJAX callbacks are not protected by the UI alone — attackers can call them directly.

Decision

If you need... Use... Why
Simple permission check _permission in route Built-in, automatic, covers 90% of cases
Complex permission logic _custom_access callback Supports multiple conditions, entity access checks
Form callback security FormStateInterface checks Validates user can access triggering element
Entity-specific access EntityAccessCheck service Proper entity operation checks (view, update, delete)

Pattern

// Route with single permission
my_module.ajax_content:
  path: '/my-module/ajax/content'
  defaults:
    _controller: '\Drupal\my_module\Controller\AjaxController::getContent'
  requirements:
    _permission: 'access content'

// Route with custom access callback
my_module.ajax_restricted:
  path: '/my-module/ajax/restricted/{node}'
  defaults:
    _controller: '\Drupal\my_module\Controller\AjaxController::restrictedContent'
  requirements:
    _custom_access: '\Drupal\my_module\Controller\AjaxController::access'
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;

public function access(NodeInterface $node, AccountInterface $account) {
  return AccessResult::allowedIf(
    $account->hasPermission('edit any article content')
    && $node->getType() === 'article'
    && $node->isPublished()
  );
}

// Form callback: verify triggering element
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
  $triggering_element = $form_state->getTriggeringElement();
  if (!$triggering_element || !isset($form[$triggering_element['#parents'][0]])) {
    throw new \Exception('Unauthorized AJAX request');
  }
  return $form['target'];
}

Reference: core/lib/Drupal/Core/Access/AccessResult.php

Common Mistakes

  • Wrong: Skipping access checks entirely → Right: AJAX callbacks are HTTP endpoints; always add protection
  • Wrong: Using _access: 'TRUE' in routes → Right: Grants unrestricted access; always use proper access control
  • Wrong: Not checking triggering element → Right: Users can manipulate requests to trigger callbacks on inaccessible elements
  • Wrong: Trusting client-side data → Right: Validate all input server-side; JavaScript validation can be bypassed
  • Wrong: \Drupal::currentUser() in wrong context → Right: Inject AccountInterface to avoid access bypass

See Also