Skip to content

Safe Markup for HTML can cause XSS in attributes in the future #4996

Open

Description

@ausi

Since #4991 it’s possible to create values that are safe for specific escaping strategies. This could suggest to the developer that proper escaping of that value in the template is now automatically taken care of securely. However this is not the case for the html strategy as this one is used in two different escaping contexts, one is as content of an element and the other is as an attribute value. Only attribute names (and unquoted attribute values) have their own strategy with html_attr.

This would enable XSS in some (not easy do notice) cases, like:

$value = new \Twig\Markup('<span title=" onload=alert(1) ">', 'UTF-8', ['html']);
{% autoescape 'html' %}
	<img src="image.png" alt="{{ value }}">
{% endautoescape %}

This is very hard to notice for the template author IMO, and in this case both the PHP and the Twig side is fully valid and securely written but still results in XSS.

If we have more and more dynamic values marked as safe for the html strategy in the future we have to split this strategy up into two separate ones: One for HTML content and one for HTML attribute values.

Activity

  1. fabpot commented on Oct 16, 2026

    @fabpot
    Contributor

    I'm not sure I understand why this is related to #4991. It's a problem, but not a new one. "Safe for html" has always meant "this is an HTML fragment". Putting one inside an attribute has always been unsafe. Using |e('html_attr') "fixes" the issue, and that's the correct way (before and after the PR).

  2. stof commented on Oct 16, 2026

    @stof
    Member

    This is a tricky case indeed: quoted HTML values require the same escaping logic than text nodes (htmlspecialchars(?, ENT_QUOTES, 'UTF-8'), which is what makes it safe to rely on the html auto-escaping for normal cases (<a href="{{ path('home') }}">) for instance as long as you quote values.
    However, an HTML fragment will need to be escaped there so that it stays an attribute value.
    Using |e('html_attr') will escape that, but will also escape much more characters (which is safe for the output, but a lot slower). We would need a strategy that escapes the same than html but does not treat HTML fragments as safe, to allow such opt-in for cases where you want to support this.
    As Twig has no knowledge of the structure of the HTML being generated (as it is not an HTML-only templating engine and cannot parse the Twig input as some kind of HTML markup as that would be a huge BC break being basically a different templating engine), we cannot do contextual auto-escaping where the auto-escaping changing the escaping strategy depending on where you render it.

  3. ausi commented on Oct 16, 2026

    @ausi
    Author

    Using |e('html_attr') "fixes" the issue, and that's the correct way (before and after the PR).

    This contradicts every template and example I know and also directly contradicts the Twig documentation in https://twig.symfony.com/doc/3.x/filters/escape.html#:~:text=HTML%20attributes%20values%20inside%20quotes :

    html: escapes a string for the HTML body context, or for HTML attributes values inside quotes.

    Also https://twig.symfony.com/doc/3.x/filters/escape.html#:~:text=use%20the%20html%20escaping%20strategy :

    …it is recommended to quote the HTML attribute value and use the html escaping strategy


    I'm not sure I understand why this is related to #4991.

    It is only partly related to that pull request, but before a general Markup value object was unsafe in many contexts so there was still a suggestion to the developer that this needs to be handled carefully. Now after the PR it looks like such an object is safe to use in templates without special care for escaping which is false and can cause security issues in the future I think.

  4. ausi commented on Oct 16, 2026

    @ausi
    Author

    We would need a strategy that escapes the same than html but does not treat HTML fragments as safe, to allow such opt-in for cases where you want to support this.

    Finding the correct backwards compatible migration path for this issue is difficult I think. Adding a new strategy like html_attr_value would require every template author to switch to that without a reliably way to deprecate using the html strategy in attribute values. We could do it the other way round and add an html_fragment strategy but that would still require every author to explicitly use foo="{{ value|e('html') }}" if they use {% autoescape 'html_fragment' %}.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions