Hi internals,
I’d like to propose a small but security-relevant addition to PHP’s
templating syntax: a new short echo tag that automatically applies
secure HTML escaping.
The problem
Despite PHP being a perfectly capable templating language on its own,
developers often reach for external templating engines (like Twig or
Blade) primarily to get automatic HTML escaping and avoid XSS. This adds
dependencies, complexity, and performance overhead — even when 95% of
the project would be simpler with native PHP templates.
At the same time, manual escaping is error-prone:
<?= $user_input ?> // ❌ dangerous
<?= htmlspecialchars($user_input, `ENT_QUOTES` | ENT_HTML5, 'UTF-8') ?>
// ✅ but verbose
Many security vulnerabilities stem from the sheer verbosity of the safe
version.
The proposal
Introduce a new short echo tag:
<?~ $expr ?>
which compiles to:
<?php echo htmlspecialchars($expr, `ENT_QUOTES` | `ENT_SUBSTITUTE` |
ENT_HTML5, 'UTF-8'); ?>
Key points:
- Uses
htmlspecialchars()(nothtmlentities()) — sufficient and standard
for XSS prevention. - Hardcodes secure flags and UTF-8 encoding (aligned with default_charset).
- Syntax is currently a parse error → no BC break.
- The <?~ tag is available whenever <?= is available.
This gives native PHP templates a secure-by-default output mechanism
without requiring a full templating engine.
Why not just use Twig/Blade?
There’s nothing wrong with those engines — but PHP itself is already a
great template language. Requiring a separate dependency just for
auto-escaping feels like overkill for many projects (e.g., small apps,
WordPress plugins, legacy modernisation).
About implementation
I’m not a C developer and don’t have experience with the Zend Engine
internals. However, I’m fully committed to:
- Writing and maintaining a complete RFC document
- Participating actively in design discussions (syntax, flags, security
model) - Testing any implementation or prototype
- Collaborating with anyone willing to help with the parser/lexer patch
If the idea gains support, I hope a volunteer from the internals team
might be interested in guiding or contributing the low-level implementation.
Next steps
I’d appreciate your thoughts — especially on:
- Whether this functionality belongs in core
- Security assumptions (e.g., hardcoded UTF-8, flag choices)
- Syntax alternatives (<?~ vs <?h= vs others)
Thank you for your time and consideration.
Best regards,
Sergei Issaev
Hi Sergei,
XSS escaping is unfortunately not as simple as that. Templating engines are
context-aware and can know whether to apply escaping for free-form text or
an attribute (which can often also be validated by type), specific tag
behaviors, and even whether the output is to be executed as HTML, XML, CSS,
JS, etc.
One-size-fits-all escaping that doesn't take such context into account is
not effective and even makes things worse by giving developers a false
sense of security.
Cheers,
Andrey.
Hi Andrey,
Thank you for the thoughtful reply — I completely agree that full
context-aware escaping (like in Twig or Blade) is essential for complex
or mixed-output scenarios.
My intention isn’t to replace that kind of intelligence, but rather to
offer a simple, safe default for the most common case: outputting plain
user-supplied text inside HTML text content or double-quoted attributes,
e.g.:
<p><?~ $comment ?></p>
<input value="<?~ $name ?>">
In these cases, htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8')
is sufficient and widely recommended. The goal isn’t to solve all XSS
vectors, but to eliminate the most frequent footgun: forgetting to
escape at all.
Developers would still be responsible for using proper context-specific
escaping (or a full templating engine) when interpolating into <script>,
styles, URLs, or unquoted attributes — but for the 80% case of rendering
form values or content in standard HTML, <?~ would provide a concise,
secure-by-default shortcut.
Think of it as similar to how <?= ... ?> made output easier than <?php
echo ... ?> — not a security feature per se, but a nudge toward safer
habits in everyday templating.
Would that narrower scope make the proposal more reasonable for core?
Best regards,
Sergei
Hi Sergei,
XSS escaping is unfortunately not as simple as that. Templating
engines are context-aware and can know whether to apply escaping for
free-form text or an attribute (which can often also be validated by
type), specific tag behaviors, and even whether the output is to be
executed as HTML, XML, CSS, JS, etc.One-size-fits-all escaping that doesn't take such context into account
is not effective and even makes things worse by giving developers a
false sense of security.Cheers,
Andrey.
Hi Sergei,
XSS escaping is unfortunately not as simple as that. Templating engines are context-aware and can know whether to apply escaping for free-form text or an attribute (which can often also be validated by type), specific tag behaviors, and even whether the output is to be executed as HTML, XML, CSS, JS, etc.
One-size-fits-all escaping that doesn't take such context into account is not effective and even makes things worse by giving developers a false sense of security.
Cheers,
Andrey.
Hi Andrey,
Which template engines are context aware? The only ones I'm aware of is my own and Latte (which take a similar approach but is quite architecturally different).
— Rob
Hi Rob,
Which template engines are context aware? The only ones I'm aware of is my
own and Latte (which take a similar approach but is quite architecturally
different).
Sorry, I don't have a direct answer to your question. I haven't written
front-end code for a loong time and haven't needed such tools recently
enough to know.
I just meant to say that only a templating engine (or something that would
parse the full output) can be context-aware. Although in many cases, where
a template engine provides helpers such as those that generate lists or
forms, those helpers still have sufficient context.
Cheers,
Andrey.
Hi!
which compiles to:
<?php echo htmlspecialchars($expr, `ENT_QUOTES` | `ENT_SUBSTITUTE` | ENT_HTML5, 'UTF-8'); ?>
htmlspecialchars is configurable for a reason and your flags are
arbitrarily different from the function default
- Syntax is currently a parse error → no BC break.
It's not, you forgot about short_open_tag=1, in which case it's
interpreted as <? (~$expr) ?>
I basically see no value over
// included earlier
function h($s)
{
return htmlspecialchars($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// template
<?= h($user_input) ?>
It's short, it does what you want, it's available today
--
Anton
- Syntax is currently a parse error → no BC break.
It's not, you forgot about short_open_tag=1, in which case it's
interpreted as <? (~$expr) ?>
Slight self-correction: with short_open_tag=0 it isn't a syntax error
either, it just echoes it to the output. This makes me think that
introducing a new short tag would be virtually impossible to do without
a major release.
--
Anton
Hi,
Thank you for the feedback — you’re absolutely right that
htmlspecialchars() is configurable for good reasons, and that a userland
helper like h() already provides a concise escape hatch today.
However, I’d like to gently push back on two points:
- <?~ vs <? (~$expr) under short_open_tag=On
You're correct that with short_open_tag=1, <?~ $x ?> would currently be
parsed as <? (~$x) ?>. But in practice:
- short_open_tag has been disabled by default since PHP 5.4 (2012).
- Most modern frameworks and coding standards explicitly discourage its use.
- The <?= short echo tag is always available regardless of
short_open_tag, precisely because it’s treated specially.
If the RFC were to propose <?~ as a new short echo variant (like <?=),
it would follow the same rule: always enabled, independent of
short_open_tag. That would eliminate the ambiguity you mentioned.
2. Why not just use h()?
Yes, h() works — and many projects already define it. But that’s exactly
the problem: everyone reinvents it, often with slightly different flags,
encoding assumptions, or error handling. This leads to:
- Inconsistent escaping across projects or even within the same codebase.
- Junior developers skipping escaping because “it’s not built in”.
- Security relying on project-specific conventions rather than
language-level defaults.
By providing a standard, secure-by-default output tag in core, PHP would:
- Reduce boilerplate.
- Encourage safer habits out of the box.
- Give small projects (e.g., WordPress plugins, scripts, internal tools)
a zero-dependency way to escape safely — without requiring them to
define or remember h().
Think of it like <?= ... ?>: it didn’t add new capability, but it made
the common case easier and more consistent. <?~ ... ?> aims to do the
same for secure output.
That said, I hear your concern about hardcoded flags. If the community
prefers, the escaping behavior could even respect default_charset and a
new html_output_flags ini setting — though I’d argue opinionated
security defaults are better here.
Thanks again for the critique — it’s helping sharpen the idea.
Best regards,
Sergei
Hi!
which compiles to:
<?php echo htmlspecialchars($expr, `ENT_QUOTES` | `ENT_SUBSTITUTE` | ENT_HTML5, 'UTF-8'); ?>htmlspecialchars is configurable for a reason and your flags are
arbitrarily different from the function default
- Syntax is currently a parse error → no BC break.
It's not, you forgot about short_open_tag=1, in which case it's
interpreted as <? (~$expr) ?>I basically see no value over
// included earlier
function h($s)
{
return htmlspecialchars($s,ENT_QUOTES| ENT_HTML5, 'UTF-8');
}// template
<?= h($user_input) ?>It's short, it does what you want, it's available today
Yes, h() works — and many projects already define it. But that’s exactly
the problem: everyone reinvents it, often with slightly different flags,
encoding assumptions, or error handling. This leads to:
- Inconsistent escaping across projects or even within the same codebase.
- Junior developers skipping escaping because “it’s not built in”.
- Security relying on project-specific conventions rather than
language-level defaults.By providing a standard, secure-by-default output tag in core, PHP would:
- Reduce boilerplate.
- Encourage safer habits out of the box.
- Give small projects (e.g., WordPress plugins, scripts, internal tools)
a zero-dependency way to escape safely — without requiring them to
define or remember h().
I can relate to these pain points, but I also think your conclusions are a
bit of wishful thinking.
Junior developers would still not use it, because they'd need to know it
first - just like h(). Even seasoned developers would avoid it because many
will deem the syntax to be ugly, and the solution imperfect. And security
gets a band-aid instead of proper protection, which is more dangerous than
you'd think - I've seen it time and time again, some feature offers limited
security benefits, people only care to remember that "it's secure", use it
for everything without critical thought, and you end up with masses of
developers believing that they write secure code when it's not even close
to that. Then PHP has to take the blame for providing insecure XSS escaping.
Cheers,
Andrey.
Yes, h() works — and many projects already define it. But that’s exactly the problem: everyone reinvents it, often with slightly different flags, encoding assumptions, or error handling. This leads to: - Inconsistent escaping across projects or even within the same codebase. - Junior developers skipping escaping because “it’s not built in”. - Security relying on project-specific conventions rather than language-level defaults. By providing a standard, secure-by-default output tag in core, PHP would: - Reduce boilerplate. - Encourage safer habits out of the box. - Give small projects (e.g., WordPress plugins, scripts, internal tools) a zero-dependency way to escape safely — without requiring them to define or remember h().I can relate to these pain points, but I also think your conclusions
are a bit of wishful thinking.Junior developers would still not use it, because they'd need to know
it first - just like h(). Even seasoned developers would avoid it
because many will deem the syntax to be ugly, and the solution
imperfect. And security gets a band-aid instead of proper protection,
which is more dangerous than you'd think - I've seen it time and time
again, some feature offers limited security benefits, people only care
to remember that "it's secure", use it for everything without critical
thought, and you end up with masses of developers believing that they
write secure code when it's not even close to that. Then PHP has to
take the blame for providing insecure XSS escaping.
You are correct that the <?~ ?> syntax alone cannot provide
comprehensive XSS protection, and it is important not to create a false
sense of security. I will remove direct mentions of XSS from the
description to avoid misleading anyone.
My main goal is not to replace contextual escaping, but to offer a
standard, recognizable syntax for a frequent operation — outputting
HTML-escaped strings. Yes, developers currently use htmlspecialchars(),
wrappers like <?=h(...)?>, etc. But it is precisely the lack of a
unified, "native" method that creates inconsistency and inconvenience.
Language evolution often follows the path of simplifying routine
operations: array() → [], cumbersome isset() checks → the concise ??.
Similarly, <?~ ... ?> could become the same standard and expected way
for safe output as <?= is for direct output. This will make the default
code for embedding dynamic content into HTML structure not only faster
to write but also instantly readable for any developer.
That is precisely why I am proposing this feature — not as a security
panacea, but as a step toward making it as quick and habitual to write
structurally correct HTML code as it is to write unsafe code.
Best regards,
Sergei Issaev
However, I’d like to gently push back on two points:
- <?~ vs <? (~$expr) under short_open_tag=On
You're correct that with short_open_tag=1, <?~ $x ?> would currently be
parsed as <? (~$x) ?>. But in practice:
- short_open_tag has been disabled by default since PHP 5.4 (2012).
But there is no plan to remove them, see
https://wiki.php.net/rfc/deprecate_php_short_tags_v2
https://wiki.php.net/rfc/counterargument/deprecate_php_short_tags
- Most modern frameworks and coding standards explicitly discourage its
use.
Since they are still a valid code, so PHP and various coding standard
tools must parse them correctly.
- The <?= short echo tag is always available regardless of
short_open_tag, precisely because it’s treated specially.If the RFC were to propose <?~ as a new short echo variant (like <?=),
it would follow the same rule: always enabled, independent of
short_open_tag. That would eliminate the ambiguity you mentioned.
No it doesn't. The grammar should stay consistent for every possible ini
state. I have a feeling that the tokenizer won't even compile with this
syntax. (not sure)
- Why not just use h()?
Yes, h() works — and many projects already define it. But that’s exactly
the problem: everyone reinvents it, often with slightly different flags,
encoding assumptions, or error handling. This leads to:
- Inconsistent escaping across projects or even within the same codebase.
- Junior developers skipping escaping because “it’s not built in”.
- Security relying on project-specific conventions rather than language-
level defaults.
That also leads to a tailored experience suitable for a specific project
and context. I also don't see an inconsistent escaping as a huge problem
as long as it does its job.
By providing a standard, secure-by-default output tag in core, PHP would:
- Reduce boilerplate.
- Encourage safer habits out of the box.
- Give small projects (e.g., WordPress plugins, scripts, internal tools)
a zero-dependency way to escape safely — without requiring them to
define or remember h().
Secure today does not mean secure tomorrow. With the existing function
you can simply update the default flag set, or users can update their
flag sets ahead of PHP. For the syntax approach to be as flexible, it
needs to be configurable by some global state (set_escape_flags()? ini
config?) which is also a big no-no, that would be magic quotes all over
again, just slightly more explicit.
Magic quotes btw should be a lesson that you can't make stuff secure by
a simple all size fits all solution.
Think of it like <?= ... ?>: it didn’t add new capability, but it made
the common case easier and more consistent. <?~ ... ?> aims to do the
same for secure output.
<?= exists since forever and "= $expr" is not a valid code. You should
at least choose another symbol (like "<?*")
That said, I hear your concern about hardcoded flags. If the community
prefers, the escaping behavior could even respect default_charset and a
new html_output_flags ini setting — though I’d argue opinionated
security defaults are better here.
See above, also respecting default_charset is a must imho, not everyone
uses UTF-8, East Asia specifically. Introducing a core syntax and
excluding a huge portion of users is not a good move.
--
Anton
Hi Anton,
Thank you for the discussion!
- short_open_tag has been disabled by default since PHP 5.4 (2012).
But there is no plan to remove them, see:
https://wiki.php.net/rfc/deprecate_php_short_tags_v2
https://wiki.php.net/rfc/counterargument/deprecate_php_short_tags
<?= exists since forever and "= $expr" is not a valid code. You
should at least choose another symbol (like "<?*").
I agree: since short tags remain valid syntax, the parser and tooling
must correctly handle them in all configuration states.
Therefore, I propose replacing <?~ ... ?> with <?: ... ?>.
See above, also respecting default_charset is a must imho, not
everyone uses UTF-8, East Asia specifically. Introducing a core syntax
and excluding a huge portion of users is not a good move.
The semantics of <?: $expr ?> would be equivalent to: <?php echo htmlspecialchars($expr, ENT_QUOTES|ENT_SUBSTITUTE | ENT_HTML5); ?> --
that is, the encoding will be determined automatically via the current
default_charset setting, as htmlspecialchars() does by default.
Best regards,
Sergei Issaev
Hi Anton,
See above, also respecting default_charset is a must imho, not
everyone uses UTF-8, East Asia specifically. Introducing a core syntax
and excluding a huge portion of users is not a good move.The semantics of
<?: $expr ?>would be equivalent to:<?php echo htmlspecialchars($expr,ENT_QUOTES|ENT_SUBSTITUTE| ENT_HTML5); ?>--
that is, the encoding will be determined automatically via the current
default_charset setting, ashtmlspecialchars()does by default.Best regards,
Sergei Issaev
I know this is not your intent, but this will give people a false sense of security. Much like Twig and Blade do today.
Here's some examples where this breaks, written in the context of an html doc:
<script> const name = "<?: $name ?>"; </script>breaks with ";alert(1); //
<a href="/search?q=<?: $query ?>"...
double encodes
window.config = <?: json_encode($config ?>
doesn't technically break ... but that would be a fun bug which would encourage devs to just turn off escaping, which defeats the purpose.
....
Maybe, you should consider something like this:
<?:html: or <?:h:
<?:attr:
<?:url:
<?:js:
<?:css:
Then you only choose the one for your context:
const name = <?:js: $name ?>
<a href="/search?q=<?:attr: $query ?>"...
window.config=<?:js: $config ?>
.size { width=<?:css: $width ?>; } //prevents ;}.size:after { content="<a..."
Its a bit more to type, but compared to googling the right way to do these in all the right places and vetting/hoping that it is right... it seems worth it.
— Rob
Am 24.12.2025 um 09:14 schrieb Rob Landers rob@bottled.codes:
I know this is not your intent, but this will give people a false sense of security. Much like Twig and Blade do today.
I think this is an important point as many on this list will object to such a feature because of this.
What I mean by this: The main obstacle is not the specific syntax but the feature in general.
I mean this as a heads-up for the original poster to be not too disappointed when (not "if" - in my personal opinion) it gets shot down.
Here's some examples where this breaks, written in the context of an html doc:
This is just the tip of the iceberg.
Maybe, you should consider something like this:
<?:html: or <?:h:
<?:attr:
<?:url:
<?:js:
<?:css:
I'm pretty sure for at least some of them one can find issues with a generic quoting, e.g. character encoding other than UTF-8.
Or what schemes are allowed for a URL, e.g. you might want to disallow javascript:.
All in all I much prefer this to be part of a framework which knows about edge cases than to be a language feature.
Or maybe something like the template strings t"foo" in Python but that's a different approach to Twig-like templates.
Regards,
- Chris
Hi all,
Maybe, you should consider something like this:
<?:html: or <?:h:
<?:attr:
<?:url:
<?:js:
<?:css:Then you only choose the one for your context:
const name = <?:js: $name ?>
<a href="/search?q=<?:attr: $query ?>"...
window.config=<?:js: $config ?>
.size { width=<?:css: $width ?>; } //prevents ;}.size:after { content="<a..."
Its a bit more to type, but compared to googling the right way to do these in all the right places and vetting/hoping that it is right... it seems worth it.
For what it's worth, that is much how Qiq works:
- {{ ... }} will not echo at all by itself
- {{= ... }} will echo raw unescaped output
- {{h ... }} will echo escaped for HTML content
- {{a ... }} will echo escaped for HTML attributes
- {{u ... }} will echo escaped for URLs
- {{c ... }} will echo escaped for CSS
- {{j ... }} will echo escaped for JavaScript
https://qiqphp.com/3.x/syntax.html
-- pmj
Hi!
Maybe, you should consider something like this:
<?:html: or <?:h:
<?:attr:
<?:url:
<?:js:
<?:css:
That’s an interesting idea! However, it goes beyond the scope of this
RFC, which aims only to introduce a concise shorthand for the frequently
used <?= htmlspecialchars(...) ?> pattern.
A more comprehensive solution with context-specific escaping—for
example, for HTML, attributes, URLs, JavaScript, or CSS—would indeed be
better implemented at the templating engine level or as a dedicated module.
Moreover, as other participants in the discussion rightly pointed out,
relying solely on automatic escaping at the rendering layer may create a
false sense of security if developers don’t fully consider the output
context in which data is used. In fact, such syntax doesn’t inherently
improve security—it primarily increases coding convenience. True safety
still depends on an explicit and context-aware choice of encoding method.
Summary of the proposal:
Introduce a new short echo tag:
<?: $expr ?>
which compiles to:
<?php echo htmlspecialchars($expr); ?>
The encoding will be determined automatically via the current
default_charset setting, as htmlspecialchars() does by default.
The behavior flags can be configured via a new INI directive:
short_echo_specialchars_flags. By default, it matches the flag values
used by htmlspecialchars().
Thank you to everyone who contributed to the discussion.
Best regards,
Sergei Issaev
<?php echo htmlspecialchars($expr,
ENT_QUOTES|ENT_SUBSTITUTE|
ENT_HTML5, 'UTF-8'); ?>htmlspecialchars is configurable for a reason and your flags are
arbitrarily different from the function default
This is the only necessary argument to reject this feature request out of
hand. Hard pass.
-Sara