Escaping Output

Pick the escaper method for the context the value lands in: HTML text, an attribute, a URL or a JavaScript string.

Syntax$escaper->escapeHtml($data, $allowedTags = null)

Escaping turns a value into something that cannot change the meaning of the markup around it. Magento\Framework\Escaper does this, and it is available in every template as $escaper. There is no single correct method, because the characters that are dangerous depend on where the value lands.

The four you need

Where the value goesMethod
HTML text between tagsescapeHtml($data, $allowedTags = null)
An attribute valueescapeHtmlAttr($string, $escapeSingleQuote = true)
A URL in href or srcescapeUrl($string)
A JavaScript stringescapeJs($string)

escapeCss($string) covers a value used inside a style rule.

Why the wrong one is still a hole

A value escaped with escapeHtml and dropped into an unquoted attribute can still close the attribute and add an event handler. A URL escaped with escapeHtmlAttr keeps a javascript: scheme intact, which escapeUrl strips. For JavaScript inside an attribute, the documentation is specific: escape with escapeJs first, then escapeHtmlAttr.

What trips people up

Escape at the point of output, not in the model, so one stored value can serve several contexts. Nothing needs recompiling for a template edit, but run bin/magento cache:clean block_html full_page to see it.

Example

Example · php
<?php
// app/code/Vendor/Notice/view/frontend/templates/notice.phtml
/** @var Vendor\Notice\Block\Notice $block */
/** @var Magento\Framework\Escaper $escaper */

$label = $block->getLabel();
?>
<div data-notice-label="<?= $escaper->escapeHtmlAttr($label) ?>">

    <?php // HTML text: escapeHtml. ?>
    <p><?= $escaper->escapeHtml($label) ?></p>

    <?php // A URI attribute: escapeUrl, which also rejects javascript: schemes. ?>
    <a href="<?= $escaper->escapeUrl($block->getNoticeUrl()) ?>"
       title="<?= $escaper->escapeHtmlAttr(__('Read more')) ?>">
        <?= $escaper->escapeHtml(__('Read more')) ?>
    </a>

    <?php // A JS string inside an attribute: escapeJs, then escapeHtmlAttr. ?>
    <button data-dismiss-text="<?= $escaper->escapeHtmlAttr($escaper->escapeJs($label)) ?>">
        <?= $escaper->escapeHtml(__('Dismiss')) ?>
    </button>

    <?php // Admin-authored rich text: allow a short tag list, nothing else. ?>
    <?= $escaper->escapeHtml($block->getBodyHtml(), ['p', 'strong', 'em', 'a']) ?>
</div>

<?php // The page renders a paragraph, a link and a button. A label of
      // <img src=x onerror=alert(1)> prints as text in all four places. ?>

When to use it

  • Print a customer-supplied name in a storefront template without opening an XSS hole.
  • Build a link from a request parameter and reject a javascript: scheme.
  • Pass a translated label into a JavaScript widget through a data attribute.
  • Render an admin-authored description that is allowed bold text and links.

More examples

The attribute mistake, wrong and right

This is the most common escaping bug in a custom template, and the reason the method name has Attr in it. escapeHtmlAttr escapes single quotes as well by default; pass false for the second argument only when you know the attribute is double quoted and a literal apostrophe matters. Quoting the attribute is still required either way.

Example · php
<?php // Wrong: escapeHtml does not escape the characters that end an attribute.
?>
<div data-sku=<?= $escaper->escapeHtml($block->getSku()) ?>>

<?php // A SKU of: x onmouseover=alert(1) x
      // produces: <div data-sku=x onmouseover=alert(1) x>
      // The browser reads onmouseover as a real event handler.
?>

<?php // Right: quote the attribute and use the attribute escaper. ?>
<div data-sku="<?= $escaper->escapeHtmlAttr($block->getSku()) ?>">

A URL that is not a URL

A target of javascript:alert(document.cookie) survives escapeHtmlAttr: the colon and the parentheses come back as character references such as &#x3A;, which the browser decodes when it reads the attribute value, so the first line still produces a working script link. escapeUrl filters the scheme as well as escaping the string. Any href, src or form action built from a request parameter, a customer record or a CMS field belongs in escapeUrl.

Example · php
<?php
// app/code/Vendor/Notice/ViewModel/Redirect.php (excerpt)
declare(strict_types=1);

public function getTargetUrl(): string
{
    // Value arrives from a request parameter, so it is not trusted.
    return (string)$this->request->getParam('target');
}

// In the template:
?>
<?php // Wrong: the attribute is safe, the scheme is not. ?>
<a href="<?= $escaper->escapeHtmlAttr($viewModel->getTargetUrl()) ?>">Continue</a>

<?php // Right: escapeUrl strips javascript: and data: schemes. ?>
<a href="<?= $escaper->escapeUrl($viewModel->getTargetUrl()) ?>">Continue</a>

Allowing a few tags in admin-authored text

With a second argument, escapeHtml parses the value and keeps only the tags you listed, rather than escaping everything. Use it for a field a trusted admin user edits, and keep the list as short as the design needs. Do not use it for customer-supplied text: a shorter list is not a sanitiser you should rely on for untrusted input.

Example · php
<?php
// app/code/Vendor/Notice/view/frontend/templates/body.phtml
/** @var Magento\Framework\Escaper $escaper */

// Body text is edited in the admin and is allowed some markup.
$allowed = ['p', 'br', 'strong', 'em', 'a', 'ul', 'li'];
?>
<?= $escaper->escapeHtml($block->getBodyHtml(), $allowed) ?>

<?php // Adding 'script' or 'img' to $allowed has no effect: escapeHtml strips
      // script, img, embed, iframe, video, source, object and audio always.
      // On <a> elements a style attribute is removed as well. ?>

Discussion

  • Be the first to comment on this lesson.