mirror of
https://github.com/twigphp/Twig.git
synced 2026-11-07 11:45:11 +00:00
Add html_attr_relaxed escaping strategy
This commit is contained in:
committed by
Fabien Potencier
parent
df893829f2
commit
04aa3df49f
@@ -1,6 +1,7 @@
|
||||
# 3.24.0 (2026-XX-XX)
|
||||
|
||||
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
|
||||
* Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names
|
||||
|
||||
# 3.23.0 (2026-01-23)
|
||||
|
||||
|
||||
+2
-2
@@ -130,8 +130,8 @@ The following options are available:
|
||||
|
||||
* ``autoescape`` *string*
|
||||
|
||||
Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``,
|
||||
``url``, ``html_attr``, or a PHP callback that takes the template "filename"
|
||||
Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``, ``url``,
|
||||
``html_attr``, ``html_attr_relaxed``, or a PHP callback that takes the template "filename"
|
||||
and returns the escaping strategy to use -- the callback cannot be a function
|
||||
name to avoid collision with built-in escaping strategies); set it to
|
||||
``false`` to disable auto-escaping. The ``name`` escaping strategy determines
|
||||
|
||||
@@ -57,6 +57,16 @@ documents:
|
||||
also when used as the value of an HTML attribute **without quotes**
|
||||
(e.g. ``data-attribute={{ some_value }}``).
|
||||
|
||||
* ``html_attr_relaxed``: like ``html_attr``, but **does not** escape the ``@``, ``:``,
|
||||
``[`` and ``]`` characters. You may want to use this in combination with front-end
|
||||
frameworks that use attribute names like ``v-bind:href`` or ``@click``. But, be
|
||||
aware that in some processing contexts like XML, characters like the colon ``:``
|
||||
may have meaning like for XML namespace separation.
|
||||
|
||||
.. versionadded:: 3.24
|
||||
|
||||
The ``html_attr_relaxed`` strategy has been added in 3.23.
|
||||
|
||||
Note that doing contextual escaping in HTML documents is hard and choosing the
|
||||
right escaping strategy depends on a lot of factors. Please, read related
|
||||
documentation like `the OWASP prevention cheat sheet
|
||||
|
||||
@@ -54,6 +54,11 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
if (\in_array('html_attr', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
$bucket['value'][] = 'html_attr_relaxed';
|
||||
}
|
||||
|
||||
if (\in_array('html_attr_relaxed', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
}
|
||||
|
||||
return $bucket['value'];
|
||||
|
||||
@@ -124,7 +124,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
|
||||
$string = (string) $string;
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'html_attr_relaxed', 'url'], true)) {
|
||||
// we return the input as is (which can be of any type)
|
||||
return $string;
|
||||
}
|
||||
@@ -256,6 +256,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $string;
|
||||
|
||||
case 'html_attr':
|
||||
case 'html_attr_relaxed':
|
||||
if ('UTF-8' !== $charset) {
|
||||
$string = $this->convertEncoding($string, 'UTF-8', $charset);
|
||||
}
|
||||
@@ -264,7 +265,12 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
|
||||
}
|
||||
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', static function ($matches) {
|
||||
$regex = match ($strategy) {
|
||||
'html_attr' => '#[^a-zA-Z0-9,\.\-_]#Su',
|
||||
'html_attr_relaxed' => '#[^a-zA-Z0-9,\.\-_:@\[\]]#Su',
|
||||
};
|
||||
|
||||
$string = preg_replace_callback($regex, static function ($matches) {
|
||||
/**
|
||||
* This function is adapted from code coming from Zend Framework.
|
||||
*
|
||||
@@ -323,7 +329,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $this->escapers[$strategy]($string, $charset);
|
||||
}
|
||||
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr', 'html_attr_relaxed'], array_keys($this->escapers)));
|
||||
|
||||
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
--TEST--
|
||||
"escape" filter does not additionally apply the html strategy when the html_attr_relaxed strategy has been applied
|
||||
"escape" filter does not additionally apply the html_attr_relaxed strategy when the html_attr strategy has been applied
|
||||
--TEMPLATE--
|
||||
{% autoescape 'html' %}
|
||||
{{ 'v:bind@click="foo"'|escape('html_attr_relaxed') }}
|
||||
{% endautoescape %}
|
||||
{% autoescape 'html_attr_relaxed' %}
|
||||
{{ 'v:bind@click="foo"' | escape('html_attr') }}
|
||||
{% endautoescape %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
v:bind@click="foo"
|
||||
v:bind@click="foo"
|
||||
@@ -179,6 +179,13 @@ class EscaperRuntimeTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testHtmlAttributeRelaxedEscapingConvertsSpecialChars()
|
||||
{
|
||||
foreach ($this->htmlAttrSpecialChars as $key => $value) {
|
||||
$this->assertEquals($value, (new EscaperRuntime())->escape($key, 'html_attr_relaxed'), 'Failed to escape: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function testJavascriptEscapingConvertsSpecialChars()
|
||||
{
|
||||
foreach ($this->jsSpecialChars as $key => $value) {
|
||||
@@ -330,6 +337,26 @@ class EscaperRuntimeTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testHtmlAttributeRelaxedEscapingEscapesOwaspRecommendedRanges()
|
||||
{
|
||||
$immune = [',', '.', '-', '_', ':', '@', '[', ']']; // Exceptions to escaping ranges
|
||||
for ($chr = 0; $chr < 0xFF; ++$chr) {
|
||||
if ($chr >= 0x30 && $chr <= 0x39
|
||||
|| $chr >= 0x41 && $chr <= 0x5A
|
||||
|| $chr >= 0x61 && $chr <= 0x7A) {
|
||||
$literal = $this->codepointToUtf8($chr);
|
||||
$this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'));
|
||||
} else {
|
||||
$literal = $this->codepointToUtf8($chr);
|
||||
if (\in_array($literal, $immune)) {
|
||||
$this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'));
|
||||
} else {
|
||||
$this->assertNotEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'), "$literal should be escaped!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCssEscapingEscapesOwaspRecommendedRanges()
|
||||
{
|
||||
// CSS has no exceptions to escaping ranges
|
||||
|
||||
Reference in New Issue
Block a user