- 后端
【免费下载链接】grav
Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony
本文以 Grav 仓库测试夹具 twig-leak-site 的 05.pageoff 页面 为切入点,讲解页面 Frontmatter 中process字段如何控制 Markdown/Twig 处理流程,以及它与全局安全开关security.twig_content.process_enabled之间的主从关系。读完你将掌握:如何为单页关闭 Twig 处理、如何理解「请求」与「允许」两层语义、Twig 关闭后{{ site.title }}之类的标记为何原样输出,以及如何在本地复现并验证这些行为。
一、起点:05.pageoff 这个测试夹具页面
tests/fake/twig-leak-site/是 Grav 核心为测试搭建的一个「Twig 泄漏(twig-leak)」对照站点,其中05.pageoff页面的完整内容只有这几行:
--- title: Page Off process: twig: false --- Twig explicitly off here, yet content has {{ site.title }} markers.信息量不大,但信息密度极高:它同时包含了处理开关声明(Frontmatter 中的process: { twig: false })和预期行为描述(正文里写入了{{ site.title }}这个 Twig 变量标记,却刻意让 Twig 处理保持关闭)。换句话说,这个夹具页就是用来验证「当页面显式关闭 Twig 处理时,正文中的 Twig 语法必须按字面文本输出,绝不能被执行」。
这种页面在 Grav 中属于合法用例:一个站点可能希望在绝大多数页面开启内容级 Twig,但对某些「纯静态」页面(如发布前的草稿、从外部系统同步进来的内容、含花括号模板片段的文档)显式关闭渲染,避免误解析。
二、twig-leak-site:一组专门「抓泄漏」的对照试验
要理解05.pageoff的位置,最好先看整个夹具站点。它共有 6 个页面,彼此构成对照组:
| 页面目录 | 核心差异 | 预期行为 |
|---|---|---|
| 01.home | 纯 Markdown,无任何 Twig 标记 | 按 Markdown 渲染即可 |
| 02.leaky | 正文含{{ site.title }},但未声明任何process标志 | 是否执行取决于安全总闸默认值(详见下文第四节) |
| 03.clean | 只有加粗等 Markdown 语法 | 与 Twig 无关的对照组 |
| 04.requested | process: { twig: true }显式请求 | 在安全门开启的前提下执行正文 Twig |
| 05.pageoff | process: { twig: false }显式关闭 | 绝不执行正文 Twig,标记原样输出 |
| 06.scan | 含{{ "hi" \| upper }}、{{ evaluate("1+1") }}及不存在的过滤器frobnicate | 沙箱/扫描逻辑的探测页:非法过滤器的失败方式、XSS 扫描触发方式 |
这套夹具的存在本身就说明:「页面正文里的 Twig 是否被执行」是 Grav 渲染管线中一个需要被刻意测试的显式决策,而不是「碰巧有没有写对语法」的偶然结果。05.pageoff正是其中「显式关闭」分支的代表。
三、processFrontmatter:每页独立声明的处理器开关
Grav 允许在每个页面的 Frontmatter 中声明该页要运行的「处理器」及其开关状态,语法是一个映射:
process: markdown: true # 是否执行 Markdown 解析 twig: false # 是否在内容上执行 Twig3.1 内置处理器只有两个
从源码看,Grav 核心目前只消费markdown与twig两个键。系统级默认值定义在 system/config/system.yaml:
process: markdown: true # 默认处理 Markdown twig_first: false # 两个都开时,是否先跑 Twig 再跑 Markdown never_cache_twig: false # 只缓存内容,永不缓存内容中处理过的 Twig注意系统配置中刻意没有写twig:键——原因在配置注释中写得很清楚:内容级 Twig 的全局开关是安全门security.twig_content.process_enabled,而不是这个process映射。process.twig只是一个「请求标志」,最终是否执行是「请求 && 允许」的与运算。
3.2 页级 Frontmatter 的合并时机
页级process是在页面 Header(Frontmatter)解析阶段逐键合并进$this->process的,见 Page.php:
if (isset($this->header->process)) { foreach ((array)$this->header->process as $process => $status) { $this->process[$process] = (bool)$status; } }也就是说,process: { twig: false }会覆盖(而不是深合并)该页此前继承的默认值——这正是05.pageoff能「铁了心」关掉 Twig 的原因。而shouldProcess()则是对外查询该开关的唯一入口(Page.php):
public function shouldProcess($process) { return (bool)($this->process[$process] ?? false); }3.3 一个常见的配置陷阱:partial 覆盖会整个替换映射
Security::applyProcessDefaults() 的注释专门警告了一个「config-merge 脚枪」:process在 Blueprint 里是一个checkboxes字段,配置合并时是整体替换而不是深合并。如果你在user/config/system.yaml里只写了process: { twig: false },就会把默认的markdown: true一起挤掉,导致全站页面输出原始 Markdown 源码。因此:
# user/config/system.yaml —— 正确写法 process: markdown: true twig: false # 或者干脆不写 twig 键,交给安全门四、安全总闸:security.twig_content.process_enabled与「请求/允许」模型
05.pageoff的process: { twig: false }是页面级请求层面的关闭,但理解它的完整语义离不开安全总闸。系统默认配置见 system/config/security.yaml:
twig_content: process_enabled: true # 主闸门:为 false 时,正文 Twig 一律不解析 editor_enabled: false # 为 false 时,只有 admin.pages_twig 权限用户能勾选页面级 twig config_access: false # 沙箱内 config 变量默认为空4.1 最终决策函数:willProcessContentTwig()
页面内容是否执行 Twig,由 Security::willProcessContentTwig() 这一处「单一事实来源」决定:
public static function willProcessContentTwig(PageInterface $page): bool { $config = Grav::instance()['config']; $gate = (bool) $config->get('security.twig_content.process_enabled', false); return ($gate && $page->shouldProcess('twig')) || $page->isModule(); }翻译成白话就是:
- 普通页面:
安全门开启 && 页面 process.twig 为 true才会处理内容 Twig; - 模块页面(modular):无条件处理(因为模块模板由主题控制,属于可信渲染路径)。
把05.pageoff代入:即使安全门开着,由于页级shouldProcess('twig') === false,正文中的{{ site.title }}也不会被解析,而是作为普通文本留在输出里。
4.2 两层的优先级关系
security.twig_content.process_enabled(默认true,自 2.0.19 起随核心开启)是站点级主闸门,同时还是页级process.twig的默认值来源(见 applyTwigContentDefault());- 页级 Frontmatter 的显式值(无论 true/false)始终优先于默认值。所以
05.pageoff即使在全站闸门打开时,仍能以twig: false独立退出渲染; - 反过来,若主闸门关闭,即使页面写了
process: { twig: true }也只是「请求」而未获「允许」,正文 Twig 依然原样输出——这正是02.leaky这类页面在闸门关闭时的表现,Admin 的「Twig in Content」报告会解释原因。
五、渲染管线:Twig 关闭时「到底发生了什么」
5.1 Page::content() 的处理分支
页面渲染时,Page::content() 会读取两个关键判定:
$process_markdown = $this->shouldProcess('markdown'); $content_twig_requested = $this->shouldProcess('twig'); $content_twig_allowed = (bool) $config->get('security.twig_content.process_enabled', false); $process_twig = Security::willProcessContentTwig($this);当$process_twig为 false(即05.pageoff的情形)时:
- Markdown 照常执行(默认
markdown: true); processTwig()(Page.php,内部调用$twig->processPage())被跳过;- 正文里
{{ site.title }}这类标记保持字面状态,最终以普通文本呈现给访客。
5.2 缓存语义:Twig 页面永不缓存处理结果
同一段源码还揭示了一个重要的缓存规则:只要页面内容要跑 Twig,就强制never_cache_twig = true(Page.php)。原因是{{ site.title }}乃至authorize()、isajaxrequest()这类表达式的结果依赖当前访问者,若把渲染结果缓存下来,就会把一个访客的输出发给下一位访客(对应安全公告 GHSA-pp89-h475-7gj6)。
因此05.pageoff这类twig: false页面反而有一个隐含收益:它是可安全缓存的内容路径——纯 Markdown 渲染结果不依赖会话/用户维度,命中缓存即输出,性能上比每次请求都重跑 Twig 的页面更优。而04.requested那种twig: true页面则每请求重渲染。
5.3 组合开关:twig_first / never_cache_twig
系统配置还提供两个与process联动的开关(system.yaml):
twig_first: true:两者都开时先跑 Twig 再跑 Markdown(Twig 输出会成为 Markdown 的输入,因此无法缓存中间态);never_cache_twig: true:只缓存 Markdown 阶段、不缓存 Twig 阶段,与twig_first互斥。
六、闸门开启时的防护网:沙箱与输出扫描
05.pageoff的对照页06.scan展示了开启路径下的探测行为:{{ "hi" | upper }}会成功执行,而{{ "x" | frobnicate }}(不存在的过滤器)与{{ evaluate("1+1") }}(受控函数)正是用来验证沙箱与扫描逻辑的探针。
当$process_twig为 true 时,正文 Twig 在沙箱中运行(security.yaml 的 twig_sandbox 节):默认只放行内置白名单(tags/filters/functions/methods/properties),站点只能通过allowed_*键加宽(合并语义是替换,因此核心把默认策略固化在代码中、只允许加法扩展,防止站点意外冻结安全基线)。违规调用会软失败并写入logs/security.log。config变量在沙箱内默认为空(config_access: false),进一步压缩信息泄露面。
此外,Security::detectXssInEditorContent() 会在保存时对「将被执行的正文 Twig」做渲染后输出扫描,命中危险标签/协议即拒绝保存。它刻意与willProcessContentTwig()使用同一判定,避免「保存时不查、渲染时才查」的漂移(对应 GHSA-2c4f-86xc-cr74 与 GHSA-fg8g-663r-f366)。对于05.pageoff这类不执行 Twig 的页面,扫描直接短路返回,无额外开销。
七、实战:本地复现与验证twig: false行为
Grav 仓库本身就是可运行的完整站点骨架,user/pages/下即真实页面目录。可按下述步骤验证:
- 查看夹具站点结构:
tests/fake/twig-leak-site/user/pages/,重点对比04.requested与05.pageoff两个页面(两者都含{{ site.title }},唯一差异是process.twig的 true/false); - 将
05.pageoff的内容复制到一个真实运行站点(如本仓库根目录的user/pages/下新建05.pageoff/default.md),保持 Frontmatter 中的process: { twig: false }; - 在
system/config/security.yaml中保持twig_content.process_enabled: true,访问该页面:正文应显示字面的{{ site.title }}(若被解析则说明配置被错误覆盖); - 再复制
04.requested的内容建对照页:此时{{ site.title }}应被替换为站点标题(site.yaml中的title值); - 将
twig_content.process_enabled改为false后重访:04.requested的标记也会原样输出——这直观展示了「页面请求 ≠ 最终允许」的两层模型。
若在 Admin 环境操作,页面编辑器的process复选框由 Security::pageProcessOptions() 动态渲染:主闸门关闭或当前用户无admin.pages_twig权限时,twig复选框根本不会出现,从 UI 层面杜绝「勾了也没用」的误导。
八、最佳实践小结
- 每页关闭:写
process: { twig: false }即可,显式值永远优先于全局默认; - 全站关闭:设
security.twig_content.process_enabled: false,此时任何页面的twig: true都只是「请求」,不会生效; - 只开少量页面:保持主闸门开启,仅在需要的页面写
process: { twig: true }——但务必只给可信编辑者授权(admin.pages_twig或editor_enabled); - 别忘 markdown:在
user/config/system.yaml覆盖process时务必同时带上markdown: true,否则整站会输出原始 Markdown; - 性能提示:
twig: false页面可安全走内容缓存,天然比每请求重渲染的 Twig 页面更快。
05.pageoff虽然只是测试夹具里的一行 Frontmatter,但它背后是 Grav「页面请求 / 安全允许」双层的 Twig 内容执行模型、渲染管线中的缓存分支决策,以及一整套沙箱与 XSS 防护网——理解它,就等于理解了 Grav 内容级 Twig 的安全边界。
- 后端
【免费下载链接】grav
Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony
相关推荐
Grav 页面内容 Twig 显式启用与泄漏检测:以 `process: twig: true` 为入口的完整防护指南
Grav 页面内容 Twig 显式启用与泄漏检测:以 process: twig: true 为入口的完整防护指南 Grav 允许在页面 Markdown 内容
后端Grav 页面内容中的 Twig 处理:process 标志、安全网关与内容泄漏防护解析
Grav 页面内容中的 Twig 处理:process 标志、安全网关与内容泄漏防护解析 导读 本文以 Grav 仓库的 twig leak site 测试站点
后端Grav 内容级 Twig 泄漏检测实战:从 twig-leak-site 的 clean 基线页看页面内容安全处理策略
Grav 内容级 Twig 泄漏检测实战:从 twig leak site 的 clean 基线页看页面内容安全处理策略 本文以 Grav 仓库测试站点 tes
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考