Security performance
Security & Performance
When to Use
When writing or reviewing Twig templates and preprocess functions — these rules apply to all template work.
Security
Autoescape — Drupal's default protection:
Drupal overrides Twig's escape filter with TwigExtension::escapeFilter(). ALL variables printed in templates are HTML-escaped unless they implement MarkupInterface (which signals they are already safe). You cannot accidentally output raw user data through normal template printing.
The |raw filter — the single biggest XSS risk in Twig:
- {{ user_content|raw }} — NEVER. User content must never be marked raw
- {{ node.field_body.value|raw }} — NEVER. Even if it's "HTML", it hasn't been filtered
- {{ node.field_body.processed }} — OK. This applies the configured text format filter
- {{ content.field_body }} — BEST. Renders with full formatter, filter, and cache pipeline
- |raw is only safe on: output from t(), render(), confirmed MarkupInterface objects, or your own generated markup wrapped in Markup::create()
URI attributes and javascript: injection:
{# WRONG — user-provided URLs can contain javascript: protocol #}
<a href="{{ node.field_link.uri }}">Link</a>
{# RIGHT — use link() which handles URL sanitization #}
{{ link(node.field_link.title, node.field_link.uri) }}
Sandbox restrictions protect against template injection:
- Twig sandbox blocks non-whitelisted methods (see Twig Overview)
- Do not add arbitrary methods to the sandbox whitelist without security review
- twig_sandbox_allowed_methods in settings.php overrides defaults — treat with care
Access control in templates:
{# WRONG — loads entity without checking access #}
{{ drupal_entity('node', 42, 'full', null, false) }}
{# RIGHT — access checked by default #}
{{ drupal_entity('node', 42) }}
{# For custom logic, check in preprocess, not template #}
Performance
Template-level caching — render arrays carry cache metadata:
- {{ content.field_name }} carries the field's cache metadata automatically
- Custom variables added in preprocess without cache metadata bypass caching → stale output
- Always bubble cache metadata from config/entities loaded in preprocess (see Adding Variables in Preprocess)
Heavy operations in templates:
- Database queries triggered by entity traversal (e.g., node.field_tags.entity) load entities lazily — each traversal may trigger a DB query
- In loops, pre-load entities in preprocess and pass an array to the template
- drupal_view() executes a full View query — avoid in loops or per-item templates
attach_library() in templates:
{# Attaches library on every render — acceptable for component-level CSS/JS #}
{{ attach_library('mytheme/my-component') }}
{# Better for libraries shared across many templates: attach in page/theme preprocess #}
Twig cache — never disable in production:
# services.yml — development only
twig.config:
debug: true
auto_reload: true
cache: false
Avoid logic-heavy preprocess that runs unconditionally:
// BAD — runs for every node render, even if view_mode doesn't need it
function mytheme_preprocess_node(&$variables): void {
$variables['related'] = $this->loadRelated($variables['node']); // Always runs
}
// GOOD — gate on view mode
function mytheme_preprocess_node(&$variables): void {
if ($variables['view_mode'] === 'full') {
$variables['related'] = $this->loadRelated($variables['node']);
}
}
Common Mistakes
|rawon any user-provided content → XSS- Not bubbling cache metadata for custom preprocess variables → stale page cache
- Entity traversal inside
{% for %}loops → N+1 query problem - Disabling Twig cache in production → 10-100x rendering slowdown
- Using
drupal_config()in twig_tweak for values that affect rendering → cache metadata not bubbled
See Also
- Cache metadata in render arrays →
drupal-render-api.md - OWASP XSS prevention:
https://owasp.org/www-community/attacks/xss/