Skip to content

Fix Escaper::escapeHtml dropping content after a stray "<" when allowed tags are passed (#37693) - #40976

Open
lbajsarowicz wants to merge 2 commits into
magento:2.4-developfrom
lbajsarowicz:fix/37693-escaper-stray-lt
Open

Fix Escaper::escapeHtml dropping content after a stray "<" when allowed tags are passed (#37693)#40976
lbajsarowicz wants to merge 2 commits into
magento:2.4-developfrom
lbajsarowicz:fix/37693-escaper-stray-lt

Conversation

@lbajsarowicz

Copy link
Copy Markdown
Contributor

Description (*)

When Escaper::escapeHtml() is called with a non-empty $allowedTags array, the input is routed through DOMDocument::loadHTML(). That branch installs a custom set_error_handler that converts any libxml parse warning into a thrown \InvalidArgumentException.

If the input contains a stray < that is not the start of a valid tag — e.g. Speed < 10m/s — libxml emits htmlParseStartTag: invalid element name, the handler throws, loadHTML() aborts mid-parse leaving a truncated DOM (only the text before the stray <), the exception is caught and logged as CRITICAL, and the truncated content is returned. So escapeHtml('Speed < 10m/s', ['b']) returns Speed and spams exception.log.

Without allowed tags the same input is handled safely by htmlspecialchars(), so the bug is specific to the allowed-tags path.

Fix: in prepareUnescapedCharacters() (which already escapes &&amp; before loadHTML()), also escape < characters that are not the start of a valid tag — i.e. not immediately followed by an ASCII letter, /, or ! — into &lt;. Real/allowed tags (<b>, </b>), and comments (<!--) are untouched; only stray < become entities, which loadHTML() parses cleanly as text. Replacement order (& first, then <) is preserved so the & inside a produced &lt; is not double-escaped.

Security impact

This change only adds escaping (stray <&lt;) and never relaxes it. Sequences that begin a real tag are unchanged and still flow through the existing allowed/prohibited-tag filtering, so no tag that was previously stripped can now pass, and no new XSS vector is introduced. Net effect on the security posture is neutral-to-positive.

Related Pull Requests

Fixed Issues (if relevant)

  1. Fixes HTML Escaper Issue #37693

Manual testing scenarios (*)

  1. In any .phtml (or via a unit call) render:
    echo $escaper->escapeHtml('Speed < 10m/s', ['b']);
  2. Before: output is Speed and a CRITICAL InvalidArgumentException: DOMDocument::loadHTML(): htmlParseStartTag ... appears in var/log/exception.log.
  3. After: output is Speed &lt; 10m/s (renders as Speed < 10m/s), no exception logged.
  4. Confirm an allowed tag still works: escapeHtml('<b>Bold</b> Speed < 10m/s', ['b'])<b>Bold</b> Speed &lt; 10m/s.

Contribution checklist (*)

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests
  • All automated tests passed successfully (all builds are green)

…ed tags are passed (magento#37693)

The allowed-tags path sends input through DOMDocument::loadHTML(), where a stray
less-than can trigger the error handler and leave the parsed DOM truncated.
Escape less-than characters that are not valid tag starts before loadHTML while
preserving real tags, closing tags, and comments.

Fixes magento#37693
@m2-assistant

m2-assistant Bot commented Jul 10, 2026

Copy link
Copy Markdown

Hi @lbajsarowicz. Thank you for your contribution!
Here are some useful tips on how you can test your changes using Magento test environment.
❗ Automated tests can be triggered manually with an appropriate comment:

  • @magento run all tests - run or re-run all required tests against the PR changes
  • @magento run <test-build(s)> - run or re-run specific test build(s)
    For example: @magento run Unit Tests

<test-build(s)> is a comma-separated list of build names.

Allowed build names are:
  1. Database Compare
  2. Functional Tests CE
  3. Functional Tests EE
  4. Functional Tests B2B
  5. Integration Tests
  6. Magento Health Index
  7. Sample Data Tests CE
  8. Sample Data Tests EE
  9. Sample Data Tests B2B
  10. Static Tests
  11. Unit Tests
  12. WebAPI Tests
  13. Semantic Version Checker

You can find more information about the builds here
ℹ️ Run only required test builds during development. Run all test builds before sending your pull request for review.


For more details, review the Code Contributions documentation.
Join Magento Community Engineering Slack and ask your questions in #github channel.

@ct-prd-pr-scan

Copy link
Copy Markdown

The security team has been informed about this pull request due to the presence of risky security keywords. For security vulnerability reports, please visit Adobe's vulnerability disclosure program on HackerOne or email psirt@adobe.com.

@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

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

Labels

Priority: P3 May be fixed according to the position in the backlog. Progress: pending review

Projects

Status: Pending Review

Development

Successfully merging this pull request may close these issues.

HTML Escaper Issue

2 participants