Think of every piece of user input as a delivery arriving at your site's front door — you don't know what's really inside the box until you open it carefully. If you display that content on a page without checking it, a malicious visitor can smuggle in a <script> tag that runs in every other visitor's browser — a Cross-Site Scripting (XSS) attack. The good news: Drupal's render system escapes almost everything for you automatically. This lesson is really about knowing exactly which situations still need your attention.
What you'll learn in this lesson
- Why most Drupal output is already XSS-safe without you doing anything
- The one very common mistake: manually escaping a value and then passing it through a
t()placeholder anyway - When you genuinely do need
Html::escape()orXss::filter()yourself
Drupal already escapes most output for you
Two mechanisms do almost all the work automatically, with zero code from you:
$this->t()placeholders — a value passed via an@variableplaceholder is escaped automatically before it's inserted into the translated string.- Render array field values — plain text values (like a form element's
#titleor a themed field value) go through Drupal's rendering pipeline, which escapes HTML by default unless you explicitly mark something safe.
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class FeedbackForm extends FormBase {
public function getFormId() {
return 'feedback_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Your Name'),
'#required' => TRUE,
];
$form['message'] = [
'#type' => 'textarea',
'#title' => $this->t('Your Message'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addMessage($this->t('Thank you, @name, for your message: @message', [
'@name' => $form_state->getValue('name'),
'@message' => $form_state->getValue('message'),
]));
}
}
@name and @message — no Html::escape() call anywhere. That's deliberate, and correct. t()'s @ placeholders already run every value through HTML escaping internally. Pre-escaping the value yourself before handing it to t() doesn't add protection — it risks double-escaping: a raw & in the user's input becomes & from your manual call, and t() then escapes that again into &amp;, corrupting the displayed text. Also replaced drupal_set_message() — removed in Drupal 8 — with $this->messenger()->addMessage().When you actually do need to escape manually
Manual escaping earns its keep in exactly one situation: when you're building a raw HTML string yourself, outside of t() placeholders and outside the normal render pipeline — for example, constructing a #markup string by hand with string concatenation:
use Drupal\Component\Utility\Html;
$safe_name = Html::escape($form_state->getValue('name'));
$build['message'] = [
'#markup' => '' . $safe_name . '',
];
Here there's no placeholder mechanism doing the escaping for you — you're hand-assembling the HTML string, so you're responsible for making sure the interpolated value can't break out of its intended position. For content that's allowed to contain some safe HTML (rather than being escaped into plain text entirely), reach for Xss::filter() instead, which strips dangerous tags/attributes while leaving an allowed subset intact.
Quick check: a visitor submits the message field with the value
Nice & simple form. If you (incorrectly) calledHtml::escape()on it before passing it throught()'s@messageplaceholder, what would the visitor actually see rendered on the page? (Answer: something likeNice & simple form— the literal escaped-entity text, not the ampersand it started as, because the value got escaped twice.)
Key takeaways
t()'s@variableplaceholders already escape their values — never pre-escape a value before passing it through one, or you'll double-escape it.- Render array field values (like
#title, themed field output) go through Drupal's rendering pipeline, which escapes by default. - Reach for
Html::escape()only when hand-assembling a raw HTML string yourself, outside the normal placeholder/render-array mechanisms. - Use
Xss::filter(), notHtml::escape(), when some HTML should be allowed through (it strips dangerous tags/attributes rather than escaping everything). drupal_set_message()is dead Drupal 7 code — use the Messenger service instead.
Coming up next
Escaping keeps displayed content safe — but it says nothing about who should be allowed to reach the form at all. Next, we'll look at validating user permissions directly inside a form's access callback, so only the right visitors ever see it in the first place.