Sanitizing User Input: Html::escape() and the End of check_plain()for Drupal 8 , 9 , 10 , and 11

Last updated :  

Think of every piece of text a visitor types into your site — a comment, a contact form message, a search query — as a stranger handing you a sealed envelope. You don't know what's really inside until you open it, and if you print its contents straight onto a public notice board without checking, you might be posting a bomb threat instead of a birthday wish. Sanitizing user input is the process of opening that envelope safely: reading what's there, stripping out anything dangerous, and only then letting it anywhere near your page.

What you'll learn in this lesson

  • Why raw user input is never safe to output directly, no matter how trustworthy the user seems
  • Which sanitization tool to reach for depending on what kind of output you're producing
  • Why check_plain() — a function you'll still see in old tutorials — no longer exists in Drupal 10/11
  • How Drupal's own render pipeline does most of this work for you automatically, if you let it

The core rule: never trust, always sanitize

Any string that originated from outside your own code — a form submission, a URL query parameter, an uploaded file's name — is "user input" for security purposes, even if it came from a logged-in, trusted administrator. The rule isn't "sanitize input from strangers"; it's "sanitize input, period," because accounts get compromised and admins make typos that happen to look like markup.

check_plain() is gone — here's what replaced it

If you've read an older Drupal tutorial, you may have seen check_plain($string) used to escape a string before output. That function was removed entirely in Drupal 8 and does not exist in Drupal 10 or 11 — calling it today throws a fatal Error: Call to undefined function. Its job was taken over by two different tools depending on context:

use Drupal\Component\Utility\Html;

// Escapes a plain string for safe inclusion in HTML — the direct
// modern equivalent of the old check_plain().
$safe = Html::escape($user_submitted_string);

Html::escape() converts special characters (<, >, &, quotes) into their HTML entity equivalents, so a string like <script>alert(1)</script> renders as inert visible text instead of executing as code.

Sanitizing at the render-array level

In practice, you rarely call Html::escape() yourself, because Drupal's render pipeline already escapes plain strings for you automatically whenever they pass through $this->t() or a #markup render array — as long as you don't mark the string as pre-trusted.

// Safe: $this->t() escapes any placeholder value automatically.
$build['message'] = [
  '#markup' => $this->t('You searched for: %term', ['%term' => $search_term]),
];

The %term placeholder tells t() to run $search_term through HTML escaping before inserting it. Placeholder prefixes matter here: @variable also escapes, %variable escapes and wraps the value in <em> tags for emphasis, and :variable is reserved specifically for URLs (it validates the value is a safe URL rather than escaping it as text) — never use :variable for anything that isn't a URL.

The dangerous shortcut to avoid: string concatenation directly into a render array's #markup, like '#markup' => 'You searched for: ' . $search_term. This bypasses t()'s automatic escaping entirely — if $search_term contains HTML, it renders as live markup, not text. Concatenation is precisely how XSS bugs sneak into otherwise careful code.

Quick check: if a contact form's "subject" field lets a visitor type anything, and you display it back in a confirmation message using $this->t('Subject: @subject', ['@subject' => $subject]), is that safe? (Yes — the @subject placeholder runs the value through HTML escaping automatically, so even if the visitor typed <script> tags, they'll render as harmless visible text.)

Key takeaways

  • check_plain() was removed in Drupal 8 and does not exist in Drupal 10/11 — use Html::escape() (from Drupal\Component\Utility\Html) as its direct replacement when you need to escape a string by hand.
  • Prefer letting the render pipeline do the escaping for you: pass dynamic values through $this->t() placeholders (@variable or %variable) rather than string concatenation.
  • :variable placeholders are for URLs only — using them for arbitrary text bypasses the escaping you actually need.
  • Treat every value that didn't originate as a hardcoded string in your own PHP as untrusted, regardless of who submitted it.

Coming up next

Sanitizing input is half the story — the other half is making sure your output stays safe even after that data has been stored and re-rendered later. Next, "Preventing XSS in Output" looks at how Twig's auto-escaping works, and the specific situations where a developer can accidentally turn it off.