news 2026/9/25 3:33:17

Twig raw 过滤器:标记输出为“安全值“以绕过自动转义

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Twig raw 过滤器:标记输出为“安全值“以绕过自动转义
  • 后端

【免费下载链接】Twig

Twig, the flexible, fast, and secure template language for PHP

项目地址:https://gitcode.com/gh_mirrors/tw/Twig
点击查看免费下载

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块内直接输出会被再次转义(如引号变成&quot;),因此需要|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行为的参照。

六、安全注意事项

  1. raw是一份"责任声明",不是"转义豁免开关"。它告诉编译器"这个值已经安全",但编译器无法替你验证值的真实性——对未净化的用户输入使用|raw会直接引入 XSS 风险。默认应让自动转义生效,仅在内容确实已经转义(如经过escape处理并存储、json_encode序列化、白名单模板生成的 HTML 片段)时才使用raw。
  2. 注意策略匹配。raw的安全声明是['all'],即声明对所有策略(html、js、css、url)都安全。若一段内容只做过 HTML 转义,却在{% autoescape 'js' %}块中用|raw输出,它实际上并未按js策略转义——此时输出是否安全取决于内容本身。
  3. 优先选择 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

项目地址:https://gitcode.com/gh_mirrors/tw/Twig
点击查看免费下载

相关推荐

上一篇:戴森球计划8000+蓝图库:从零开始打造高效星际工厂的终极指南
下一篇:conda init 命令详解:Shell 初始化机制、参数全解与源码级实现分析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 3:30:15

ACM模式Java输入输出全攻略:从Scanner到快读模板

刷题刷到一定阶段&#xff0c;你就会发现一个绕不开的坎&#xff1a;ACM模式。这个词在Java面试题和算法题库里反复出现&#xff0c;很多在IDE里写惯了LeetCode式核心代码的朋友&#xff0c;第一次在笔试系统里碰见要自己处理输入输出的题目时&#xff0c;当场就懵了。键盘倒是…

作者头像 李华
网站建设 2026/9/25 3:30:02

AI记忆系统设计实战:从会话上下文到跨会话长效记忆

1. 从“AI 失忆”说起&#xff1a;为什么记忆是智能的最短木板做过 NLP、跑过对话系统、搭过智能客服的朋友&#xff0c;大概率都遇到过同一个尴尬场景&#xff1a;模型上一轮还能准确回答“我叫小明&#xff0c;今年 28 岁”&#xff0c;下一轮换个句式问“我多大了”&#xf…

作者头像 李华
网站建设 2026/9/25 3:29:21

谢希仁计算机网络课件:可运行、可验证、可调试的教学活体切片

简介&#xff1a;本资源是谢希仁《计算机网络》第6版&#xff08;“十二五”国家级规划教材&#xff09;配套的完整课件PPT&#xff0c;面向高校电气信息类、计算机类本科生及研究生&#xff0c;也适用于网络工程技术人员系统复习核心理论与协议体系。课件共1173页&#xff0c;…

作者头像 李华