Skip to content

Security

When to Use

  • You're handling user-generated content in components
  • You're passing data from untrusted sources
  • You're working with attributes and HTML markup

Decision

Twig auto-escapes all output by default. Use the Attribute object for dynamic attributes. Prop schemas are a development-time lint. They are not a runtime defence and must never be the thing standing between user input and your markup.

Three reasons it does not hold: - The validation call is assert($this->doValidateProps($context, $component_id)); (ComponentsTwigExtension.php:106). On a production zend.assertions=-1 PHP compiles the call away. Nothing runs. - Even with assertions on, ComponentValidator::validateProps() takes the context by value and returns a bool (:172). A failing prop is reported, never corrected or removed — the bad value still reaches the Twig. - The context is narrowed to declared prop names before validating (:189-190), so a prop nobody declared is never examined at all.

pattern, format, enum and minLength are excellent for catching your own integration mistakes early and for describing the API to tooling. Use them for that — not for keeping malicious input out.

Pattern

Pattern: Auto-Escaping in Twig

Twig auto-escapes all output by default.

Reference: Drupal Security Documentation

{# Auto-escaped by default #}
<h1>{{ title }}</h1>  <!-- Safe: HTML entities encoded -->

{# Explicitly mark as safe (only for trusted markup) #}
<div>{{ content|raw }}</div>  <!-- DANGER: No escaping -->

WHY auto-escape is critical: Prevents XSS attacks. User input automatically sanitized unless explicitly marked safe.

Pattern: Attribute Object for Safe Attribute Handling

Always use Attribute object for dynamic attributes.

# In component schema
props:
  type: object
  properties:
    attributes:
      type: Drupal\Core\Template\Attribute
{# Safe: Attribute object handles escaping #}
<div{{ attributes.addClass('my-class') }}>

{# DANGER: Manual attribute string concatenation #}
<div class="{{ classes }}">  <!-- Vulnerable to XSS -->

WHY use Attribute: Handles proper escaping of attribute values, prevents injection attacks.

Anti-Pattern: Props Validation as a Security Control

# This does NOT keep a javascript: URI out of the href.
props:
  type: object
  properties:
    url:
      type: string
      format: uri
      pattern: '^https?://'

Sanitize at the boundary instead. Whatever produces the value — preprocess, controller, formatter — is responsible for it:

use Drupal\Component\Utility\UrlHelper;

// Strip javascript:, data:, vbscript: etc. before the value becomes a prop.
// UrlHelper::stripDangerousProtocols() (core/lib/Drupal/Component/Utility/UrlHelper.php:402)
$url = UrlHelper::stripDangerousProtocols($raw);

and in the template, prefer Drupal's own escaping over trusting the prop:

{# Attribute handles URI escaping; it does not whitelist schemes — filter first #}
<a{{ attributes.setAttribute('href', url) }}>{{ text }}</a>

Pattern: Sanitizing User Input

For user-generated content in props (rare), sanitize before passing.

// In preprocessing or controller
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;

$build = [
  '#type' => 'component',
  '#component' => 'my_theme:card',
  '#props' => [
    // Plain text: HTML escape
    'title' => Html::escape($user_input),

    // Allow limited HTML: filter tags
    'description' => Xss::filter($user_input, ['p', 'br', 'strong', 'em']),
  ],
];

Pattern: Safe Markup in Slots

Slots should contain render arrays or safe markup objects.

// Proper slot content handling
'#slots' => [
  'content' => [
    '#markup' => $filtered_html,  // Already sanitized
    '#allowed_tags' => ['p', 'br', 'strong'],
  ],
],

Common Mistakes

Common Mistake: Using |raw filter on user-generated content. WHY: Disables auto-escaping. Only use on trusted, pre-sanitized markup from Drupal render system.

Common Mistake: Building attributes as strings instead of Attribute object. WHY: String concatenation doesn't escape attribute values. Use Attribute object for safe attribute handling.

Common Mistake: Not validating props with schema in development. WHY: Invalid data passes through in production without validation. Development is the only place a schema violation can be seen, so always test with assertions enabled.

Common Mistake: Treating a prop schema as a security boundary. WHY: It is assert()-gated, non-mutating, and skips undeclared props entirely. Filter and escape at the source; let the schema catch integration mistakes, not attackers.

See Also