☰
Grav 页面 `process: twig: false` 完全指南:用 Frontmatter 精准关闭页面级 Twig 渲染
2026/10/14 2:20:32 网站建设 项目流程
  • 后端

【免费下载链接】grav

Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony

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

本文以 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.requestedprocess: { twig: true }显式请求在安全门开启的前提下执行正文 Twig
05.pageoffprocess: { 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 # 是否在内容上执行 Twig

3.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的情形)时:

  1. Markdown 照常执行(默认markdown: true);
  2. processTwig()(Page.php,内部调用$twig->processPage())被跳过;
  3. 正文里{{ 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/下即真实页面目录。可按下述步骤验证:

  1. 查看夹具站点结构:tests/fake/twig-leak-site/user/pages/,重点对比04.requested与05.pageoff两个页面(两者都含{{ site.title }},唯一差异是process.twig的 true/false);
  2. 将05.pageoff的内容复制到一个真实运行站点(如本仓库根目录的user/pages/下新建05.pageoff/default.md),保持 Frontmatter 中的process: { twig: false };
  3. 在system/config/security.yaml中保持twig_content.process_enabled: true,访问该页面:正文应显示字面的{{ site.title }}(若被解析则说明配置被错误覆盖);
  4. 再复制04.requested的内容建对照页:此时{{ site.title }}应被替换为站点标题(site.yaml中的title值);
  5. 将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

项目地址:https://gitcode.com/gh_mirrors/gr/grav
点击查看免费下载
上一篇:Kepubify Web版本使用指南:在线转换Kobo电子书的完整教程
下一篇:300亿参数开源模型刷新效率纪录:Step-Video-T2V Turbo如何重构文生视频行业规则?

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询