- 后端
【免费下载链接】Twig
Twig, the flexible, fast, and secure template language for PHP
raw是 Twig 中用于标记变量为"安全值"的过滤器:在启用了自动转义(auto-escaping)的环境中,被raw作为最后一个过滤器处理的变量将按原样输出、不再被转义。本文基于 raw 过滤器官方文档 展开,并结合当前仓库源码解释它"零运行时成本"的实现原理、raw必须位于过滤器链末端的原因,以及它与Markup类、escape过滤器等安全机制的协同关系,帮助你在模板中安全地输出已转义内容(如json_encode的结果)而不产生二次转义。
一、raw 过滤器的基本用法
原始文档(doc/filters/raw.rst)给出的核心语义只有一句话:raw过滤器将值标记为 "safe"(安全)。具体含义是:在启用自动转义的环境中,当raw是该变量上应用的最后一个过滤器时,该变量不会被转义:
{% autoescape %} {{ var|raw }} {# var won't be escaped #} {% endautoescape %}autoescape标签本身的作用域与策略(html、js、false等)详见 autoescape 标签文档。文档中强调了一个容易忽略的细节:"ifrawis the last filter applied to it"(当raw是最后应用的过滤器时)。这不是文档措辞的随意性,而是由安全值(safe analysis)的编译期推导规则决定的,这一点会在第三节结合源码解释。
一个实用的推论:在未启用自动转义的环境中,{{ var|raw }}与{{ var }}的输出完全相同——因为raw本质上是一个编译期的"身份变换",它不会改变值本身,只是向编译器声明"这个值已经安全"。
二、源码实现:raw 是一个零成本的编译期标记
2.1 过滤器注册:没有可调用对象
raw过滤器并非由 CoreExtension 提供,而是由负责转义体系的 EscaperExtension 注册:
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),这一行注册代码包含三个关键信息:
callable为null:raw没有任何运行时实现,它不需要对值做转换;'is_safe' => ['all']:静态声明该过滤器对所有转义策略(html、js、css、url等)都"安全",这正是EscaperNodeVisitor判断"该表达式是否需要再包一层escape"的依据;'node_class' => RawFilter::class:解析raw时生成的不是通用的FilterExpression节点,而是专门的 RawFilter 节点。
2.2 RawFilter:compile 时只做"透传"
RawFilter 继承自FilterExpression,其compile()方法的全部逻辑只有一行:
public function compile(Compiler $compiler): void { $compiler->subcompile($this->getNode('node')); }即:编译{{ var|raw }}时,只会把内层表达式var编译进输出,不会生成任何调用raw函数的 PHP 代码。换言之,raw过滤器在运行时的开销为零,它的全部效果都发生在模板编译阶段——由节点访问器(NodeVisitor)读取is_safe => ['all']这个元数据来决定是否跳过转义包裹。
另外,FilterExpression::compile() 中保留了一段针对raw的兼容分支,并自 Twig 3.11 起发出弃用提示:"Creating the 'raw' filter via 'FilterExpression' is deprecated; use 'RawFilter' instead."。这说明当前版本中节点访问器必须为raw构造RawFilter实例,而不能走通用过滤器节点路径。
三、"为什么 raw 必须是最后一个过滤器":安全值的推导规则
3.1 转义决策发生在编译期
是否对某个PrintNode(即{{ ... }}输出语句)包上escape过滤器,由 EscaperNodeVisitor 在遍历 AST 时决定:
- 进入
AutoEscapeNode时把当前转义策略压入statusStack(enterNode),离开时弹出;没有autoescape标签时则回落到环境的默认策略(getDefaultStrategy,可通过FileExtensionEscapingStrategy等机制按文件扩展名设置,见 src/FileExtensionEscapingStrategy.php); - 处理
PrintNode时调用escapeExpression(),核心判断是isSafeFor($type, $expression, $env)(leaveNode、isSafeFor):
return \in_array($type, $safe, true) || \in_array('all', $safe, true);只要表达式的"安全集合"包含当前策略或'all',就跳过escape包裹。而表达式的安全集合由 SafeAnalysisNodeVisitor 计算。
3.2 过滤器链的安全集合是"从外到内"传播的
SafeAnalysisNodeVisitor 对 FilterExpression 的处理 揭示了raw位置敏感性的根源:
$safe = $filter->getSafe($node->getNode('arguments')); // ... if (!$safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); } $this->setSafe($node, $safe);- 若过滤器自身声明了安全策略(如
raw声明了['all']),整个过滤器表达式就直接安全; - 若外层过滤器未声明安全(如
upper、e),则安全集合等于"内层表达式的安全集合"与"外层过滤器保持安全的能力(preservesSafety)"的交集——通常交集为空。
由此可以精确理解文档中"raw 是最后一个过滤器才有效"的含义:
{{ safeHtml|raw }} {# raw 在最外层 → 安全集合为 ['all'] → 不转义 #} {{ safeHtml|raw|upper }} {# 外层 upper 未声明安全,交集为空 → 仍会被转义 #} {{ var|e|raw }} {# raw 在最外层 → 不再转义(e 的转义仍保留在值里) #}这也是为什么raw常被写在过滤器链的末尾——它是"最终裁决",而不是中间步骤。
3.3 与 escape 过滤器的协同
escape(别名e)过滤器注册时使用了is_safe_callback(见 EscaperExtension),运行时实现位于 EscaperRuntime。autoescape 文档 还提到两条相关规则,与raw的用法直接相关:
- 不会二次转义:Twig 足够智能,不会用
escape过滤器对"已按相同策略转义过"的值再次转义; - 静态表达式不转义:
{% set hello = "<strong>Hello</strong>" %}{{ hello }}会原样输出<strong>Hello</strong>,因为编译器能识别字面量是安全的。
四、与 Markup 类、安全类注册表的对比
除模板内用|raw标记外,Twig 还支持在PHP 侧声明一个值是安全的,典型代表是 Markup 类:
class Markup implements \Countable, \JsonSerializable, \Stringable { private $content; private ?string $charset; public function __construct($content, $charset) { ... } }其类注释(src/Markup.php)明确说明:Markup实例(及既有子类)被视为"已被判定可安全输出的内容",在 Twig 沙箱中对其方法调用与属性访问会绕过SecurityPolicy的允许清单。此外,EscaperRuntime 维护了一份安全类注册表(safeClasses/safeLookup,通过addSafeClass()注册),从源码结构看,运行时转义会借助该注册表识别Markup这类"天生安全"的值并跳过转义。
可以这样对比两种"声明安全"的途径:
| 途径 | 位置 | 适用场景 |
|---|---|---|
{{ value\|raw }} | 模板中 | 值在模板上下文里已确认安全(如缓存的 HTML 片段、json_encode输出) |
Markup实例 /addSafeClass() | PHP 代码中 | 在业务层就确定某类对象的内容已转义,统一注册后模板侧无需再写raw |
注意raw只影响转义决策,它本身不改变值的类型;而Markup还会附带沙箱信任等运行时语义。两者配合时,Markup值再经过|raw输出仍是安全的,但实践中二选一即可。
五、实战示例:来自集成测试的真实用例
当前仓库的集成测试用少量用例覆盖了raw的典型使用姿势:
- 基础用例(tests/Fixtures/filters/raw.test):
{{ br|raw }}数据为['br' => '<br>'],期望输出<br>——变量排除了自动转义。
JSON 场景(tests/Fixtures/filters/json_encode.test):
{{ "foo"|json_encode|raw }}。json_encode生成的 JSON 串本身不是 HTML 安全内容,若在autoescape块内直接输出会被再次转义(如引号变成"),因此需要|raw收尾。URL 场景(tests/Fixtures/filters/urlencode.test):
{{ {...}|url_encode|raw }},同理防止&等字符被 HTML 策略二次转义。与
apply标签组合(tests/Fixtures/tags/apply/json_encode.test):
{% apply json_encode|raw %}test{% endapply %}说明raw位于apply过滤器链的末端,与文档"最后一个过滤器"的规则一致。
这些用例由 tests/IntegrationTest.php 统一驱动执行,可直接作为验证raw行为的参照。
六、安全注意事项
raw是一份"责任声明",不是"转义豁免开关"。它告诉编译器"这个值已经安全",但编译器无法替你验证值的真实性——对未净化的用户输入使用|raw会直接引入 XSS 风险。默认应让自动转义生效,仅在内容确实已经转义(如经过escape处理并存储、json_encode序列化、白名单模板生成的 HTML 片段)时才使用raw。- 注意策略匹配。
raw的安全声明是['all'],即声明对所有策略(html、js、css、url)都安全。若一段内容只做过 HTML 转义,却在{% autoescape 'js' %}块中用|raw输出,它实际上并未按js策略转义——此时输出是否安全取决于内容本身。 - 优先选择 PHP 侧注册安全类:如果某类对象的内容在业务层就保证已转义,用 Markup 或
EscaperRuntime::addSafeClass()统一声明,可以显著减少模板中散落各处的|raw,让"安全边界"集中在代码侧管理。
小结
raw过滤器的完整工作链路是:EscaperExtension 以is_safe => ['all']注册且callable为null→ 解析器生成 RawFilter 节点(编译时仅透传子表达式,零运行时开销)→ SafeAnalysisNodeVisitor 将"最外层过滤器安全集合为['all']"记入 AST → EscaperNodeVisitor 据此跳过escape包裹。理解这条链路后,你就能准确把握"为什么raw要放在过滤器链末尾",并把它与Markup、escape、autoescape策略正确组合,在"防转义过度"与"防 XSS"之间取得平衡。
- 后端
【免费下载链接】Twig
Twig, the flexible, fast, and secure template language for PHP
相关推荐
Twig nl2br 过滤器详解:HTML 换行转换与自动转义的前置转义机制
Twig nl2br 过滤器详解:HTML 换行转换与自动转义的前置转义机制 本文以 Twig 官方文档中的 nl2br 过滤器为切入点,完整讲解其在模板中的用
后端终极LFI过滤器绕过指南:2024年最新Payload与实战技巧
终极LFI过滤器绕过指南:2024年最新Payload与实战技巧 Local File Inclusion(本地文件包含,LFI)漏洞是Web应用安全中最常见且
网络安全应用安全渗透测试告别手动复制粘贴:3步高效获取国家中小学智慧教育平台电子课本
告别手动复制粘贴:3步高效获取国家中小学智慧教育平台电子课本 你是否曾经为了获取电子课本而反复复制粘贴网址?是否因为下载速度慢、文件命名混乱而烦恼?tchMat
网页爬虫教育
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考