Repository navigation
[RFC] Drop |raw in favor of a more secure approach #4754
Description
I would like to propose deprecating (and in future drop) the use of |raw which is quite a radical change of the current concept, I'm very much aware of that 😊 But bear with me for a second here - I named it RFC for a reason 😉
Here's why I think the concept of |raw in templates is fundamentally flawed.
If you take a step back and think of what {{ variable|raw }} does, the first thought that comes to your mind because of how the filter is named is probably:
Hello Twig, output
variableexactly as it is, I know it is safe here.
And this thought is exactly what I would like to challenge.
Because in projects where you write the controllers and you control the templates. you know exactly what input you have, where you pass the variables, in what context etc. - you are in full control.
Hence, this makes you think (!) that {{ variable|raw }} is safe and you can ouput it |raw.
But can you?
Here's the thing: I think, this assumption is wrong.
Because very often, the ones that write the templates and the ones that use them (aka write the controllers), are not the same people.
Imagine you have template designer Alice that designs a table.html.twig:
<table>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>That's great, developer Bob can now use this template when writing his controller:
$rows = [
['Cell 1', 'Cell 2', 'Cell 3'],
['Cell 4', 'Cell 5', 'Cell 6'],
];
echo $twig->render('table.html.twig', [
'rows' => $rows,
]);Nice! Now two weeks later, developer Eve also needs to output content formatted as a table, in a completely different context. She needs to output user input in one of the cells, though:
$rows = [
['Cell 1', 'Cell 2', 'Cell 3'],
['Cell 4', 'Cell 5', $request->query->get('user-input')],
];
echo $twig->render('table.html.twig', [
'rows' => $rows,
]);Well, no problem. She checks the template, sees that it contains <td>{{ cell }}</td> and knows that Twig's output encoding is going to take care of that properly. All good!
Now another two weeks later, developer Bob comes back to his controller and his task is to wrap Cell 1 into a <strong>Cell 1</strong>.
So he asks template designer Alice to adjust the template to allow for that.
Now what does template designer Alice do?
If we are lucky, she knows (or can see from the source code, if the project lives in the same git repository) that developer Eve passes untrusted input and so she'll go for
<table>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell|sanitize_html }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>Or she implements some special logic where one can say which cells should be wrapped in <strong></strong> (but then again, what if tomorrow someone needs another formatting option?).
Phew! That was close! But what if she did not know this template was used elsewhere? What if there's a general repository for some company CI/CD and re-usable Twig templates used in all sorts of projects?
Would she go for
<table>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell|raw }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>and thus introduce a security vulnerability without knowing it? 💥
And here's why I think that
Hello Twig, output
variableexactly as it is, I know it is safe here.
is always the wrong thing to say. You never want to allow |raw values which essentially translates to "whatever format".
What you actually want to say in this case is
Hello Twig, if
variableis safe for the current escaping strategy (sohtmlbecause derived fromtable.html.twig), you can output it raw. If not, please apply output encoding for current escaping strategy.
The template designer never has this information. So in my opinion, it's inherently wrong to ask them to add |raw.
As the template designer, what you want is: {{ variable|rawIfEscapingStrategiesMatch }} 😊
But you always want that, so might as well drop the filter alltogether 😉
Imho, we should invert the logic and move this responsibility to the one using the template (aka the controller).
Imagine something along the lines of this (I know a lot of it is missing, it's just for illustration purposes):
class TemplateVariable
{
public function setContentForEscapingStrategy(string $content, string $strategy): self
public function getContentForEscapingStrategy(string $strategy): mixed
}Now there's no more |raw so our template designer Alice doesn't have to worry. We're back to
<table>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>Developer Bob can still do this in the simplest case:
$rows = [
['Cell 1', 'Cell 2', 'Cell 3'],
['Cell 4', 'Cell 5', 'Cell 6'],
];
echo $twig->render('table.html.twig', [
'rows' => $rows,
]);For his <strong> case, he now needs to do this:
$cell1Content = new TemplateVariable();
$cell1Content->setContentForEscapingStrategy('<strong>Cell 1</strong>', 'html'); // Bob knows this is safe, it's HIS responsibility!
$rows = [
[$cell1Content, 'Cell 2', 'Cell 3'],
['Cell 4', 'Cell 5', 'Cell 6'],
];
echo $twig->render('table.html.twig', [
'rows' => $rows,
]);Twig would now check if {{ cell }} represents an instance of TemplateVariable and fetch the content for the current escaping strategy which is html. If it's configured like in this case, it can be output as is, because the strategies match. If it's not, then the escaping strategy needs to be applied just as usual.
And what would Eve do to use that same template? Well:
$cell6Content = new TemplateVariable();
$cell6Content->setContentForEscapingStrategy($this->sanitizer->sanitize($request->query->get('user-input')), 'html'); // Eve knows this is safe now
$rows = [
['Cell 1', 'Cell 2', 'Cell 3'],
['Cell 4', 'Cell 5', $cell6Content]
];
echo $twig->render('table.html.twig', [
'rows' => $rows
]);You see, the responsibility to tell Twig which variables are safe for what strategy, must not be the template designer's. Using |raw effectively disables all output encoding which is never what you want as a template designer. You don't want to allow "everything". You want to allow "everything that is safe".
So all you should care about as a template designer is to write your templates with the correct escaping strategies in mind:
{# Ensure this template is called `button_with_js.html.twig` because we want `html` to be the default escaping strategy for this template #}
<div>
<p>{{ value }}</p>
<button title="{{ value|e('html_attr') }}">Click</button>
<script>
const msg = "{{ value|e('js') }}";
console.log(msg);
</script>
</div>And here we go. This template can now be used by any developer in that project safely.
In its simplest form, you can just use it like you do it today. Pass the value as a string:
echo $twig->render('button_with_js.html.twig', [
'value' => 'I am not dangerous',
]);And if you want value to contain Bob "The Builder" <script>doSomethingSafe()</script>, because you know that it's safe for e.g. html, you have to take care of things yourself:
$value = <<<'TXT'
Bob "The Builder" <script>doSomethingSafe()</script>
TXT;
$variable = new TemplateVariable();
$variable->setContentForEscapingStrategy($value, 'html'); // I know this is safe for "html", I want the script to be executed - for all the other strategies, escape normally.
echo $twig->render('button_with_js.html.twig', [
'value' => $variable,
]);Not sure if this was presented logically and the examples make sense 😄
But I think it's good enough to make my point.
- Currently
|rawoutputs the contents alwaysrawno matter the escaping strategy. - I think this is never what you want. You want to always output things safely no matter the strategy.
- Template designers should not think about the contents of
variables. They should think about the context they are outputting those and apply the correct escaping filters there (so nothing new for them actually). - Developers that make use of those templates need a way to mark a variable as being safe for a certain strategy
- This shifts responsibilities to where they belong
Activity
stof commentedon Feb 4, 2026
This proposal is similar to
\Twig\Markup, except that it associates an info about which strategy should be considering the value as safe (currently,\Twig\Markupis considered safe for any strategy).nicolas-grekas commentedon Feb 4, 2026
No need to deprecate
raw: we can very well say that both sides should express that rawness: only vars labelled as safe could be used with raw.About deprecating eventually, dunno if that's required. We could also make this opt-in. We could even add a flag to trigger a deprecation when a template or a controller don't agree on the safety so help template/controller authors to move to the stricter mode.
Toflar commentedon Feb 4, 2026
That's correct. Essentially
|rawcurrently equals\Twig\Markup. You can also already mark certain classes as safe using theaddSafeClass()on theEscaperRuntime- but not for certain strategies dynamically only.In other words:
|rawwould not mean "raw" but "raw if marked safe for the current strategy".We could but then again, I think from a template designer's point of view it should just never be needed, imho.
Also, it would be super easy for tools like the Twig CS fixer to prohibit the use of
|rawif it did not have 2 different behaviors depending on some configuration value.I totally agree. I think this is an implementation detail. How the classes are named/how you pass variables marked as safe for certian strategies and whether or not to deprecate
|rawor make it opt-in or what not is secondary.I think what should be done in the first place is discussing whether that idea/concept/approach even makes sense or if there are conceptual issues with my thoughts (which I'm definitely not ruling out) 😉
stof commentedon Feb 4, 2026
you don't need a dedicated filter for that. If it is marked safe, the auto-escaping will not escape the value again, without any filter.
The
rawfilter has only 1 effect: marking a value as safe for auto-escaping (without actually changing the value).If you don't want to allow marking arbitrary values as safe, you can forbid that filter in your linter.
stof commentedon Feb 4, 2026
it does not have 2 different behaviors.
Toflar commentedon Feb 4, 2026
I know that I can do that. I'm questioning today's state of things and whether it should be changed for better security/usability for everyone.
I didn't say it has. It would have if a configuration that influences its behavior was introduced as suggested by Nicolas.
stof commentedon Feb 4, 2026
There is no configuration to be introduced.
Making
rawusable only on values that are already safe is totally useless, as that's the same than removing the filter.nicolas-grekas commentedon Feb 4, 2026
Well that's a bit direct, and wrong, if I may 😅
That's not the same because it is visible when reading a template: this var contains markup. That is important to me.
aschempp commentedon Feb 5, 2026
But isn't the point here that you – as a template developer – might not know whether a variable contains markup or not?
nicolas-grekas commentedon Feb 5, 2026
As a template dev, you have to know which var exists for what, and which ones will output markups, otherwise you won't be able to know what to do with them, that's not new and we can build on that IMHO.
Toflar commentedon Feb 5, 2026
I appreciate the passionate comments so far. After all, it's Twig. It's been around for years and we've all come to love it for what it is so questioning established practices always meets resistance to some degree. But hey, I did an RFC, I requested those comments, so all good! 😊
I do think, that we can all agree on one thing, though: Twig is meant to make things safe(r) out of the box by applying output encoding by default compared to using just plain old
<?= $variable =>in PHP directly.What I am proposing is indeed to drop the
rawfilter because I'm just convinced that a template designer never wants to accept "raw" values but instead "safe" values. I just don't see a use case where I, as the template designer, ever want to output anything just "raw". What I want is not "raw". What I want is "safe"!I think my
button_with_js.html.twigexample is the perfect illustration for that:Template designers already have to think about how and where to encode the variables correctly. It's what we've been doing forever. It's the current status quo.
When you want to use a variable within JavaScript, you already had to encode it properly using
|e('js'). And I think this is perfect. Because only the template designer knows in what context the variable is used. So they have to give Twig the information on how to encode this variable properly.Which I guess is exactly what @nicolas-grekas meant with
If you did, then I think we're on the same page! As the template dev, I have to give Twig the information - and now I am asking myself, when would I ever use
rawthen?To me,
rawis not an encoding hint to Twig. It's "hello Twig, just don't touch this variable at all!".And I'm just not sure there's a use case for that. Because - at least in this example - what I want is not
rawcontent. I want to output the contents ofvaluesafely encoded forhtml. Always.So in fact, I would have to write
because I know this is within the context of
htmland whatevervaluecontains - I don't want to output itrawbut properly escaped forhtml. Just like I do forjs,css,html_attretc.The only reason we're not having to write
<p>{{ value|e('html') }}</p>over and over again is because every template comes with a default output encoding strategy for convenience. Which is - always has been - and always will be a brilliant decision. It makes writing templates easy and safe by default.My humble assumption as to why
|rawexists and we're using it, is not because we need that filter to exist. But because we're lacking a way to inform Twig about the fact that a given variable is already safe for a given context.Today, we mainly pass on scalars. We just do
If we had the option to do
then I don't see why we would need the
rawfilter anymore. By default, things are just always encoded using either the default strategy or if hinted using|e('whatever-strategy'), thewhatever-strategystrategy. And if I want to prevent that Twig encodes my variable (aka double encoding), I have to tell Twig "hey, this variable is already safe forwhatever-strategy".Just one more thing: My intention is not to break anybody's workflow that's been established over the course of the last decade or more! I don't want to take away your precious
rawfilter!If there are use cases and thus good reasons to keep
|rawthen we should do that. No doubt.The reason why I created this issue and am now investing a lot of time in trying to explain my thoughts is simple: My intention is to make Twig even more secure by default if we can. And I think we can - it just requires questioning the status quo sometimes 😊
stof commentedon Feb 5, 2026
To me, what we need first is to introduce the ability to provide a markup object (either directly in
Twig\Markupor in a separate class depending on BC concerns) that knows for which strategy that markup was produced (instead of considering it safe for all of them).This would solve your use case (currently, using a
Twig\Markupwould not escape it invalue|e('js')due to the fact that it is considered safe for all strategies).I don't think we should build an object that contains multiple representation of the markup, as this adds lots of complexity (if you pass the value to some functions or filters, which representation should be used to cast it to string ?). Your example seems to use some markdown string with an HTML representation. A better solution for that IMO is to use the
markdownfilter in the template to produce the HTML representation (and that filter is already integrated with auto-escaping rules)Removing
rawentirely from Twig is a separate topic IMO. Projects wanting the safer usage of Twig can already ban the usage of this filter in a linter.stof commentedon Feb 5, 2026
|rawdoes not indicate "this variable contains markup". It indicates "I want to allow markup in this variable without any safety check" (could also be described in the more scary way "disable safety checks on that variable"). You can have variables that contain safe markup that will be rendered fine withoutraw.aschempp commentedon Feb 6, 2026
I think that's exactly what is being proposed here. And if that "object" exists, and everyone uses it correctly (that's a very wide assumption 😆) then raw would no longer be necessary. Because
|rawis only necessary if you basically want to bypass the auto encoding (htmlin most cases). Keeping|rawin the first place certainly wouldn't harm though (and obviously would be necessary for backwards compatibility for a long time).stof commentedon Feb 26, 2026
I have a few (rare) cases where I indeed want to bypass the auto-escaping logic. I don't think we should deprecate that filter entirely. But we should definitely discourage using it for most cases.
If starting today (without the ecosystem cost of a BC break), I would suggest naming that filter
unsafe_bypass_escapingor something like that, to make it clear that such usage needs careful review (a bit like Rust'sunsafeblocks)8 remaining items
Toflar commentedon Apr 29, 2026
Indeed, that would need to be adjusted too. Let's frame this RFC differently then: I'm not just talking about the
|rawfilter - I just think the template language - for security reasons - should have no such concept asrawakano context. Because there's always a context and the template designers should annotate every output of a variable with the correct context.Clean separation, data lives in a JSON script block, executable code only reads and parses it 😊 Either way, I think this example is solvable.
vladko312 commentedon Apr 29, 2026
As I mentioned, when the string to not be escaped would only be obtained inside the template. There is no way for the developer to mark the string safe, if it is not yet created.
As a template designer, you should communicate to the developers which variables allow which markup inside them, so that the developers can decide for themselves, how they would protect such values from unsafe user input. Forcing developers to mark something safe on their end will not solve the problem, as developers might mark something as safe, just because they need it to contain markup. It might even make things worse, as now the developers can disable autoescaping of any variable on their end.
If the designer wants to insert a variable in a script or attribute, they absolutely should escape it according to the context using
|e(...). But this is not a usecase for a|rawfilter to begin with, why would they WANT to use it? They CAN mark everything as|rawfor no reason, but that would be JUST to make it vulnerable, not for some contexts. The|rawfilter is used when the template author INTENDS the variable to contain markup of the context it is in. For example, for rendering HTML from a string. The template author can not ensure, that the HTML is safe, but rendering it is intentional. The developer would then be able to pass any HTML, including user-supplied values, but that would be his responsibility, as he passed user data into a variable that is supposed to contain only safe data.The
|rawfilter is used to indicate that the template author wants the value to become part of the produced markup without escaping, as that value already contains the markup. It is an intentional feature that such a value can affect the markup of the template. Using ANY escaping would break that markup. It is the template designer who decides which variable is supposed to have markup in it, and the developers would know from the context that the variable should contain it.You are talking about a different use-case, where it is the developer who wants to insert markup in a regular value (e.g.
<strong>). For this usecase, some way to disable escaping on the developer side should be added. But if the value is already supposed to have markup in it, which is a primary use-case for|raw, it would always be the responsibility of the developer to sanitize user-supplied values when adding them to the markup inside the string. In that case, it does not matter, who marks the value as safe.vladko312 commentedon Apr 29, 2026
The
|rawfilter is not the same asno context, it is used to disable escaping so that the value can affect the context. Escaping on the other hand is used to prevent the value from affecting the context. Correct escaping should be used to insert text inside html file, js string or a tag parameter. The|rawis used to insert HTML markup or JS code. It does not matter WHO marks them as safe, they must BE safely created. It is not the problem of escaping or the|rawfilter, as that value is already supposed to contain something that can do anything on the page.vladko312 commentedon Apr 29, 2026
A way to solve such a problem inside the template would be inserting data into the string on the template side:
That way,
cell_markupcan affect the HTML structure, whilecell_datais safely escaped to prevent injections.Toflar commentedon Apr 29, 2026
Which is what I think is exactly what is wrong in the current approach. In the templating world there is no such thing as
without escapingbecause every variable is always output in a context - no matter what. You can never have no escaping. Never. You can just have a variable that is already escaped aka marked safe so it becomes a noop.Exactly. Which is why the template designer must never mark it
{{ html_content|raw }}but{{ html_content|e('html') }}and the developer using the template has to decide that the variable is safe for HTML. Because only they can know if it is or not.I completely disagree with this. Which is the whole point of this RFC.
Of course there's always someone who can do the wrong thing - we cannot prevent people from producing bugs. That's not the point.
The point is that right now - and this is the reason for this RFC - the responsibility is on the wrong side. The only person in the whole wide world who's able to mark a variable safe for context
x,yorzis the one who knows the content of the variable -> the backend dev. And the only person in the whole wide world who knows that in the templatehtml_contenthas to be encoded forhtmlis the template dev. So let's shift the responsibilities to where they belong.I’m going to step back from the discussion for a while now. It doesn’t make sense to keep restating my point of view when everything I have to add is already there. I believe I’ve clearly explained why I think the current approach is not the right one, so I’ll leave the floor to others. Reiterating the same arguments wouldn’t add any further value to the conversation.
vladko312 commentedon Apr 29, 2026
It feels like you completely misunderstand the purpose of the
|rawfilter and escaping. Escaping is used to safely embed the value inside the context. If you want to add some text safely inside HTML, you use|e('html'). The escaping can not be used to "safely" add the HTML itself, it is only used to add values in the context. The|rawfilter does not mark the value as "already escaped", it marks it as "unescaped, but trusted". Such a value should not be escaped, as escaping it would turn markup into equivalent text, rather than insert it into the page.{{ html_content|e('html') }}is just applying the default escaping and has the same result as{{ html_content|e }}or{{ html_content }}, it does not allow inserting HTML into the page and is not a replacement of{{ html_content|raw }}When
|rawis used, the variable is already supposed to contain HTML markup that would be injected into the page, there is no magical escaping that would differentiate the intended markup from the unintended one, and protecting such a value is not related to the template engine at all, the same way that template engine or template author is not responsible for protecting something like<script src="{{script_src}}">.You make a good argument for why
|rawshould not be used in places, where it is already not needed, not a good argument about removing|rawin it's primary usecase.vladko312 commentedon Apr 29, 2026
Main use-case for
|rawis to inject HTML, that the developer passed, not to prevent double escaping of some string.Template designer already intends to inject HTML, and the developer is already responsible for protecting HIS html from untrusted input.
vladko312 commentedon Apr 29, 2026
Even in the first example with Bob and Eve, it is not the correct way to use
|raw. Alice changed the meaning of the value and it's security context (a breaking change), without even any communication in the changelog. This way, Alice introduced a vulnerability by her own actions, not through the|rawfilter. A similar vulnerability would appear if a programmer of a different system (say, Charlie) would move some value from SQL prepared statement value list to the query string in order to allow injectingORDER BY, as requested by Bob. Eve would still pass user-supplied values as it was safe before. Same would happen if Charlie would decide that the value needs to be put throughevalfirst, as Bob wants it that way.Alice (like Charlie) should decide who is responsible for what, and communicate the safety context of each variable. There is nothing wrong with injecting something directly, if it is properly communicated and others know, who is responsible for protecting the values. The problem is not
eval, SQL queries or|raw, it is the lack of communication about responsibility.aschempp commentedon May 8, 2026
Lets try to step away from context theory and clarify some technical things:
foo.html.twigtemplate will usehtmlas the default escaping strategy (assuming auto-escaping is turned on). So all values output like{{ foo }}are exactly the same as{{ foo|e('html') }}.{{ foo|raw }}, you're telling Twig to skip|e('html')under these circumstances.auto_escapingis turned off, there is no difference between{{ foo }}and{{ foo|raw }}is_safe => ['html']), it behaves exactly the same as|raw. Twig will know the default escaping context, and it would not do anything to the value.is_safe => ['json'], it would however be encoded in a.html.twigtemplate, or a variable as|e('html')What @Toflar is proposing is that it should be possible to mark a variable as
is_safe => ['html'](or any other context obviously). Exactly the same as already possible in functions and filters. If my controller correctly escapes a value, it should be able to let the template engine know the work has already be done / the value is safe.vladko312 commentedon May 8, 2026
Allowing that is a good idea, but it is not a good reason to remove the
|rawfilter. The filter is applied to the expression before output, not just to the initial values. And unlike filters and functions that are provided by the programmer to be used as is, values are processed by the template that works as code for them. And it is usually the template author that decides where and how the values would be presented, including if the values are just data or contain part of the output structure.Template can be treated as the code in a special language, where functions/filters are dependencies and values are input.
aschempp commentedon May 18, 2026
see #4801 for the simple code changes to make this work.
bug twigphp#4825 Make the include() function return a Markup object (…
stof commentedon Oct 16, 2026
Allowing Markup to be safe only for some strategies is being implemented in #4991