Security-Related Standards
Some coding standard violations are just a matter of style. This chapter covers the ones that aren't — the mistakes that put a site's actual security at risk, not just its readability.
1. Escaping Output
Anything that ends up printed to the page and came from a user — a form field, a query string, an uploaded file's name — has to be escaped before it's output, or it becomes a script injection waiting to run in someone else's browser.
<script>alert('test')</script>If a value like that gets printed as-is, the script runs. Twig's autoescaping (covered in the Twig Standards chapter) handles this automatically for template output; PHP code that builds markup directly reaches for Html::escape() or Xss::filter() instead of concatenating raw input into a string.
2. Sanitizing Allowed HTML
Not everything needs to be escaped into inert text — a body field is often allowed some HTML, just not all of it. Xss::filter() strips anything outside an allow-list of safe tags, which is the difference between sanitizing and simply not printing the value at all.
// ✅ Correct: a limited set of tags survives, everything else is stripped.
$safe_markup = Xss::filter($user_submitted_html, ['a', 'em', 'strong']);3. Placeholders in Translatable Strings
t() and $this->t() take a placeholder array rather than a concatenated string, and the placeholder's prefix character controls how it gets treated: @value is escaped as plain text, %value is escaped and wrapped for emphasis, and :value is inserted unescaped — reserved specifically for a URL that's already known to be safe.
// ❌ Incorrect: user input concatenated directly into the string.
$this->t('Welcome ' . $username);
// ✅ Correct: the placeholder value is escaped automatically.
$this->t('Welcome @username', [
'@username' => $username,
]);4. Form API and CSRF Protection
A form built with the Form API gets CSRF protection automatically, through a token Drupal generates and validates without the module having to do anything extra — which is one more reason a form is always built with the Form API rather than raw HTML, even for something as small as a single button.
5. Access Checks
A route, a block, or a piece of custom logic that shows or does something sensitive checks access explicitly, rather than assuming that because a link isn't shown, the route behind it is unreachable. Entity operations go through $entity->access('view') (or update, delete); routes declare their requirement in the routing YAML.
// ❌ Incorrect: assumes the hidden link is enough protection.
public function build() {
return ['#markup' => $this->renderSensitiveData()];
}
// ✅ Correct: access is actually checked.
public function build() {
if (!\Drupal::currentUser()->hasPermission('view sensitive data')) {
throw new AccessDeniedHttpException();
}
return ['#markup' => $this->renderSensitiveData()];
}6. Validating File Uploads
An uploaded file is validated against an explicit extension allow-list — never a deny-list — and the validators that come with the File and Image field types cover the common cases (extension, file size, image dimensions) without needing to be reinvented per module.
$validators = [
'FileExtension' => ['extensions' => 'gif png jpg jpeg'],
'FileSizeLimit' => ['fileLimit' => '5 MB'],
];