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.
#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@subjectplaceholder 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 — useHtml::escape()(fromDrupal\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 (@variableor%variable) rather than string concatenation. :variableplaceholders 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.