From drupal-core
Provides OWASP security patterns for Drupal 10/11 including SQL injection prevention, XSS filtering, route access control, and custom checkers. Use for code security reviews and hardening.
npx claudepluginhub ajv009/drupal-devkitThis skill uses the workspace's default tool permissions.
```php
Provides OWASP security patterns for Drupal 10/11 including SQL injection prevention, XSS filtering, route access control, and custom checkers. Use for code security reviews and hardening.
Reviews code for OWASP Top 10 vulnerabilities, input validation, auth flows, security headers, CSRF/XSS prevention, and dependency audits.
Audits web applications and REST APIs for OWASP Top 10 vulnerabilities including broken access control, authentication failures, and data protection. Use when reviewing code, auth/authz, APIs, or before deployment.
Share bugs, ideas, or general feedback.
// Bad: string concatenation.
$result = $connection->query("SELECT * FROM {node} WHERE title = '$title'");
// Good: parameterized query.
$result = $connection->query(
"SELECT * FROM {node} WHERE title = :title",
[':title' => $title]
);
// Best: Entity API.
$nids = \Drupal::entityQuery('node')
->condition('title', $title)
->accessCheck(TRUE)
->execute();
// Twig auto-escapes by default - safe.
{{ node.title }}
// Explicit escaping for raw output.
use Drupal\Component\Utility\Html;
$safe = Html::escape($user_input);
// Xss filter for allowed HTML.
use Drupal\Component\Utility\Xss;
$filtered = Xss::filter($user_input);
// Admin filter (more tags allowed).
$filtered = Xss::filterAdmin($content);
# my_module.routing.yml
my_module.admin:
path: '/admin/my-module'
defaults:
_controller: '\Drupal\my_module\Controller\AdminController::page'
requirements:
_permission: 'administer my_module'
my_module.content:
path: '/my-module/{node}'
defaults:
_controller: '\Drupal\my_module\Controller\ContentController::view'
requirements:
_entity_access: 'node.view'
declare(strict_types=1);
namespace Drupal\my_module\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
final class MyAccessChecker implements AccessInterface {
public function access(AccountInterface $account): AccessResult {
return AccessResult::allowedIfHasPermission($account, 'access my_module')
->cachePerPermissions();
}
}
Routes can have multiple _*_access* requirements that ALL must pass (AND logic). Don't assume _permission is the only gate.
# Example: three access checkers on one route
my_module.entity_edit:
path: '/entity/{entity}/edit'
requirements:
_entity_access: entity.update # Entity-level check
_custom_archived_check: 'TRUE' # Custom: is entity archived?
_custom_status_check: 'TRUE' # Custom: additional gate
Debugging 403s when entity access passes:
requirements in *.routing.yml_*_access* checker service in *.services.ymlDuring security review: Examine ALL route requirements, not just _permission. Custom access checkers may silently block access even when entity-level access is granted.
// Form API handles CSRF automatically via form tokens.
// For custom AJAX endpoints:
use Drupal\Core\Access\CsrfTokenGenerator;
// Generate token.
$token = \Drupal::csrfToken()->get('my_module_action');
// Validate token.
if (!\Drupal::csrfToken()->validate($token, 'my_module_action')) {
throw new AccessDeniedHttpException();
}
$validators = [
'file_validate_extensions' => ['pdf doc docx'],
'file_validate_size' => [25 * 1024 * 1024], // 25MB
'file_validate_name_length' => [],
];
Html::escape).\Drupal:: in classes).\Drupal:: static calls in service classes.