Skip to content

[Stempler] Encode on* attribute output as a JavaScript literal - #1299

Merged
roxblnfk merged 2 commits into
masterfrom
fix/stempler-js-context-escaping
Aug 5, 2026
Merged

[Stempler] Encode on* attribute output as a JavaScript literal#1299
roxblnfk merged 2 commits into
masterfrom
fix/stempler-js-context-escaping

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Aug 5, 2026

Copy link
Copy Markdown
Member

What was changed

Context-aware escaping now encodes both JavaScript contexts as JavaScript literals.

on* attributes were encoded with htmlspecialchars() and wrapped in literal &quot;. They are now encoded with the same JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT flags already used for <script>, with the delimiters HTML-encoded afterwards.

Invalid UTF-8 no longer collapses the value: JSON_INVALID_UTF8_SUBSTITUTE was added to the on* 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: &quot; and &#039; decode back into real quotes, and a quote in the value ends the JavaScript string literal instead of remaining part of it. The JSON_HEX_* flags leave no HTML-special character inside the literal, so the value survives both decoding passes — and the outer htmlspecialchars() 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: HTMLSyntax marks only on* and style attribute values as Verbatim, and style is excluded by the branch condition. Ordinary attributes (title, href, data-*) keep the plain htmlspecialchars() filter, where HTML-entity encoding is the correct choice.

Separately, json_encode() returns false on 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_SUBSTITUTE mirrors the ENT_SUBSTITUTE the default filter has always used.

Rendered output is unchanged for values that needed no encoding — {{ "hello world" }} in an onclick still renders alert(&quot;hello world&quot;) — so only the compiled PHP differs.

Checklist

  • Closes #
  • Tested
    • Tested manually
    • Unit tests added

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 survives json_encode() with these flags, so the outer htmlspecialchars() cannot double-encode: values round-trip back to the original through html_entity_decode() + json_decode(), including already-encoded input such as &amp;.

Documentation

The "Context-Aware escaping" section of docs/en/views/stempler.md documents only <script>; it is worth extending with the on* case now that both contexts use JavaScript encoding.

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 &quot;,
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
roxblnfk merged commit 23bd2bb into master Aug 5, 2026
13 checks passed
@roxblnfk
roxblnfk deleted the fix/stempler-js-context-escaping branch August 5, 2026 10:29
@roxblnfk
roxblnfk restored the fix/stempler-js-context-escaping branch August 5, 2026 10:39
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)
@roxblnfk

roxblnfk commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@Yoskaldyr you might be interested this fix

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant