[Stempler] Encode on* attribute output as a JavaScript literal - #1299
Merged
Conversation
An on* attribute holds JavaScript delivered inside HTML, so the browser HTML-decodes the value before the JS engine parses it. The finalizer encoded it with htmlspecialchars() and wrapped it in literal ", which the browser decodes back into real quotes — quotes in the value end the JavaScript string literal instead of staying part of it. The parser marks only on* (and style) attribute values as Verbatim, so this branch covers event handlers exclusively. Encode the value as a JavaScript literal with the same JSON_HEX_* flags already used for <script>, then HTML-encode the delimiters. Those flags leave no HTML-special character inside the literal, so the outer htmlspecialchars() only affects the surrounding quotes and the rendered output is unchanged for values that needed no encoding. Broken UTF-8 degrades to U+FFFD through JSON_INVALID_UTF8_SUBSTITUTE, mirroring the ENT_SUBSTITUTE the default filter already uses. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…alue json_encode() returns false on malformed UTF-8, so the compiled template echoed an empty string and the value vanished from the emitted JavaScript, changing the arity of the surrounding call. JSON_INVALID_UTF8_SUBSTITUTE mirrors the ENT_SUBSTITUTE already used by the default filter: broken sequences become U+FFFD and the value keeps its shape. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
roxblnfk
added a commit
that referenced
this pull request
Aug 5, 2026
[Stempler] Encode on* attribute output as a JavaScript literal (cherry picked from commit 23bd2bb)
Member
Author
|
@Yoskaldyr you might be interested this fix |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was changed
Context-aware escaping now encodes both JavaScript contexts as JavaScript literals.
on*attributes were encoded withhtmlspecialchars()and wrapped in literal". They are now encoded with the sameJSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOTflags already used for<script>, with the delimiters HTML-encoded afterwards.Invalid UTF-8 no longer collapses the value:
JSON_INVALID_UTF8_SUBSTITUTEwas added to theon*branch (first commit) and to the<script>branch (second commit).Three existing assertions on compiled PHP were updated, and four cases were added to
DynamicToPHPTest.Why?
An
on*attribute value is JavaScript delivered inside HTML, so the browser HTML-decodes it before the JS engine parses it. HTML-entity encoding therefore does not hold:"and'decode back into real quotes, and a quote in the value ends the JavaScript string literal instead of remaining part of it. TheJSON_HEX_*flags leave no HTML-special character inside the literal, so the value survives both decoding passes — and the outerhtmlspecialchars()is left with nothing to encode except the surrounding quotes.This branch is reached for event handlers exclusively, which is why the fix is scoped to them:
HTMLSyntaxmarks onlyon*andstyleattribute values asVerbatim, andstyleis excluded by the branch condition. Ordinary attributes (title,href,data-*) keep the plainhtmlspecialchars()filter, where HTML-entity encoding is the correct choice.Separately,
json_encode()returnsfalseon malformed UTF-8, so the compiled template echoed an empty string and the value vanished from the emitted JavaScript, silently changing the arity of the surrounding call.JSON_INVALID_UTF8_SUBSTITUTEmirrors theENT_SUBSTITUTEthe default filter has always used.Rendered output is unchanged for values that needed no encoding —
{{ "hello world" }}in anonclickstill rendersalert("hello world")— so only the compiled PHP differs.Checklist
Manual testing covered both branches across strings, quotes, apostrophes, integers, floats,
null, booleans, lists, associative arrays, non-ASCII text, and malformed UTF-8, comparing the rendered attribute against what a browser yields after HTML-entity decoding. A byte-by-byte sweep confirmed that no HTML-special character survivesjson_encode()with these flags, so the outerhtmlspecialchars()cannot double-encode: values round-trip back to the original throughhtml_entity_decode()+json_decode(), including already-encoded input such as&.Documentation
The "Context-Aware escaping" section of
docs/en/views/stempler.mddocuments only<script>; it is worth extending with theon*case now that both contexts use JavaScript encoding.