漏洞

WordPress:模板名怎样越出主题目录

CVE-2026-87902 让 WordPress 把解码后的页面名称当成模板路径,特定主题和 PHP 环境下可进一步执行代码;已有公开攻击记录,当前应更新到 7.1.2,旧分支按对应补丁处理,并核查修复前是否留下异常文件。

暖纸色的页面排版盒中,一张模板纸条穿过分隔板,连到盒外的代码页。
文章导航

9 月 22 日,WordPress 发布 7.1.2,修复了一个不需要登录就能触发的模板解析漏洞。访客提供的页面名称经过解码,被拼进模板文件名;在符合条件的主题下,这条路径可以越出主题目录,让 PHP 加载服务器上的其他 PHP 文件。9 月 25 日,CISA 将 CVE-2026-87902 加入已知利用漏洞目录。留给管理员的工作已经包括补丁部署和修复前的入侵排查。

当前上游更新目标是 WordPress 7.1.2。应优先处理能从公网访问、仍在使用受影响核心版本,并且活动子主题或父主题里有 page- 开头目录的站点。官方公告举出的例子包括 page-templates,以及采用这类布局的 Twenty Twelve、Twenty Fourteen、Neve、Hestia 和 Sydney。实际危害还取决于服务器上有哪些可读的 PHP 文件,以及它们被加载后会做什么。不能只看主题名称决定是否更新,所有受影响核心都应纳入修复。

我们的判断是,符合这些条件的站点应把升级与历史排查并行推进。补丁能阻止新的越界模板选择,已经写入的文件、被改动的账号和泄露的凭据需要另外处理。这个问题也解释了一类容易漏掉的错误:一个适合用于查询的名称,在后续解码和拼接之后,获得了操作文件系统的含义。

1 页面名称怎样进入模板选择

WordPress 显示一个页面时,先查出页面内容,再寻找负责排版的模板。经典主题可以给单个页面指定模板,也可以按页面名称或 ID 提供文件,最后回落到通用的 page.php。例如,一个名为“关于”的页面,可以使用 page-关于.php。这让主题作者可以直接给特定页面安排版式。

漏洞涉及的解码步骤最初服务于这项功能。2016 年的 提交 e7b0581为非 ASCII 名称增加了解码后的候选模板,并把包含表情符号的名称加入相关测试。4.6.1 的 get_page_template() 只拼接原名称,4.7.0 已经包含新增分支。以普通中文为例,%e5%85%b3%e4%ba%8e 解码为“关于”,随后可以匹配磁盘上的中文文件名。兼容需求本身很具体,问题出在解码后的内容没有重新接受路径检查。

请求入口允许传入 pagename 和 page_id。在修复前的 7.1.1、提交 a940fbc1e63a7e24d31a28e707e545ad4873cb84 中,WP::parse_request() 的 319–336 行会接收允许的查询变量。GET 与 POST 都可以提供这些字段;同一个字段同时出现在两处且值不同,会被判为变量不匹配,返回 400。后续分析需要保留这个条件,不能把两种输入说成无条件相互覆盖。

这两个字段随后承担了不同工作。WP_Query::get_posts() 的页面名称分支先按名称查找页面,并对名称做整理;到 2276–2280 行,符合分支条件的 page_id 可以把 SQL 条件改为按 ID 选择页面,之前的 pagename 仍保留在查询变量中。于是,一个请求可以取得真实页面,同时让模板选择继续使用请求提供的名称。页面必须确实可访问,首页、文章页、附件和插件改写也会影响实际走到哪条分支。

名称整理为何没挡住后面的路径变化?sanitize_title_with_dashes()需要保留合法的百分号编码,以支持 URL 中的非英文名称。字面点号等字符会被处理,编码形式仍可能留在字符串里。查询阶段看到的是名称文本;模板阶段再次调用 urldecode() 时,编码内容才变成有路径含义的字符。这两步使用同一个值,却依赖不同的安全条件。

真正生成危险候选的是 wp-includes/template.php 中 get_page_template() 的 471–504 行。已经由页面指定的自定义模板会先进入候选列表;接着是解码名称、原名称、页面 ID 和通用模板。下面保留了旧版的关键分支。

// WordPress 7.1.1,get_page_template() 节选
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
    $templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";

page- 前缀和 .php 后缀限定了路径的形状。要沿这条分支走出目录,路径首先需要经过一个实际存在、名称以 page- 开头的目录,随后才可能通过上级目录组件继续解析。这就是官方把主题顶层目录布局列为前提的原因。一个完全不同的高优先级模板先被选中,也可能改变当前请求的结果。资产识别因此需要同时看核心版本、活动主题目录和实际模板层级。

2 文件存在,随后就会交给 PHP

候选列表经 get_query_template()进入模板查找。7.1.1 的 locate_template()依次尝试活动子主题目录、父主题目录和核心的 theme-compat 目录。它把候选名称接在目录后面,调用 file_exists();第一个存在的文件就成为查找结果。这一步没有确认解析后的文件仍属于允许的目录。

路径里的上级目录组件由文件系统处理。前面已经加过主题目录,并不能保证最终落点仍在那里。攻击者还需要一个确实存在、能被 Web 服务账号读取的本地 PHP 文件。文件不存在、权限拒绝,或者前面的模板候选已经命中,都会影响这一请求能否到达预期目标。

最终执行位置在 wp-includes/template-loader.php 的 114–132 行。它在 template_include 过滤器之后调用 realpath(),确认路径是可读的普通文件、扩展名符合要求,然后执行 include $template。这次 realpath() 得到了规范路径,却没有把结果与主题目录比较。PHP 文件中的代码会按正常的包含语义执行;其输出和副作用取决于所选文件。

请求中的 pagename
  → 查询变量保留编码后的页面名称
  → get_page_template() 解码并生成候选
  → locate_template() 找到存在的本地文件
  → template-loader.php 校验文件属性
  → include 执行该 PHP 文件

区块模板会影响这条路径。locate_block_template()可能选中区块模板并返回模板画布,也可能保留已有的 PHP 候选。主题支持声明、模板优先级和过滤器都参与选择。因此,我们用经典 PHP 主题解释直接路径,同时保留官方的目录条件;“使用区块主题”本身不足以替代一次版本和配置核查。

官方公告给出的进一步执行链使用服务器上的 pearcmd.php。这是 PEAR 的命令行入口;当 Web PHP 环境启用 register_argc_argv 时,请求内容可以进入它读取的参数结构。公告将官方 PHP Docker 镜像,以及 PHP 低于 8.5 时的默认 cPanel 配置列为相关环境。实际容器和主机可能改过配置,排查必须看处理网站请求的 PHP-FPM 池或 Apache 模块,登录终端看到的 CLI 配置只能说明那个 CLI 进程。

由此可以区分两个影响层次。漏洞直接提供的是选择并包含本地 PHP 文件的能力;要让它变成攻击者希望的任意代码执行,还需要可用文件及相应配置。关闭 register_argc_argv 可以阻断公告描述的 PEAR 参数链,模板越界包含仍需核心补丁修复,其他本地文件的行为也需要按实际环境判断。

页面名称解码后进入主题模板候选,旧查找只判断文件存在,可能落到主题外的 PHP 文件;7.1.2 检查解码名称,并限制含上级目录组件的候选路径。

左右滑动查看图中说明。

图 1:模板查找的执行路径示意。省略号代表未展开的候选名称。图中的“允许目录”包含主题及代码明确保留的兼容目录,具体检查范围见下一节。

3 补丁检查名称,也检查最终落点

9 月 22 日合入的 修复提交 170944a只改了 wp-includes/template.php,增加了两处相互补充的检查。以下使用 7.1.2 的固定提交 0a106cde38df869e196a83eb4975ba38a4e5837b。第一处就在解码之后,491–498 行要求 validate_file() 返回零,才把解码结果加入模板列表。

// WordPress 7.1.2,get_page_template() 节选
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename
    && 0 === validate_file( $pagename_decoded ) ) {
    $templates[] = "page-{$pagename_decoded}.php";
}

validate_file()会整理路径分隔符,拒绝其规则识别出的上级目录穿越和 Windows 驱动器路径等输入。正常的中文名称仍可通过。检查放在解码之后,使函数看到即将被拼入路径的字符,修补了原先名称检查与文件使用之间的断点。

第二处在共享查找函数中。新版 locate_template()找到存在的候选文件以后,还要通过 _wp_is_template_path_allowed(),才会返回路径。候选被拒绝时,循环继续检查后面的模板名称,站点仍有机会使用正常的回退模板。

这个新函数的范围值得读清。713–760 行先在规范化路径中查找 .. 形式的目录组件;没有这类组件的现有路径直接通过。需要深入检查时,函数用 realpath() 解析真实位置,再与允许目录的真实路径比较。比较前保留末尾分隔符,避免把同名前缀的另一个目录误当成目录内部。

允许列表包括活动子主题、父主题和 wp-includes/theme-compat。主题标识本身位于子目录时,代码还接受相应主题的直接父目录,用来保留已有布局的兼容性。因此,这个补丁针对模板穿越增加了明确约束;它的快速通过分支、兼容目录和可信插件过滤器仍有各自的语义。把它描述成“所有 PHP 模板都被关进活动主题目录”,会高估实际保证,也会误导自定义主题的验收。

我们另外对照了 4.7.36 与 4.7.37、6.8.9 与 6.8.10、7.0.5 与 7.0.6 的同一文件。它们都加入了解码后校验和查找后的路径约束;旧分支使用适合其 PHP 兼容要求的常量和字符串函数。其余分支的修复版本依据厂商发布表,本文没有逐个重做源码核对。该修复提交本身没有新增测试文件,2016 年的名称兼容测试也不能被当成本次安全修复的动态验证记录。

4 更新版本,临时措施留在请求入口

WordPress 当前上游版本和本次首次修复版本都是 7.1.2。官方还为从 4.7 开始的旧分支提供了补丁,完整对应关系在 GHSA 公告。下表列出当前分支和几个旧分支的对应关系;已经安装相应旧分支修复版的站点,不应再被“所有低于 7.1.2 的版本都受影响”这类粗略规则误报。

上游分支本次受影响范围该分支首次修复
7.17.1.0–7.1.17.1.2,当前上游更新目标
7.07.0.0–7.0.57.0.6
6.96.9.0–6.9.86.9.9
6.86.8.0–6.8.96.8.10
6.16.1.0–6.1.136.1.14
4.74.7.0–4.7.364.7.37;其他旧分支见公告

旧分支这次收到补丁,不代表它重新获得持续维护承诺。WordPress 在发布说明中明确,只有最新版本受到积极维护,旧版补丁属于对仍在使用这些版本的用户提供的帮助。短期兼容性要求可以决定先装哪个回移版本,长期更新目标仍应回到受维护版本。4.6 及更早版本也不能因为本次新增解码分支尚不存在,就作为安全回退目标。

发行版打包的站点还要看包维护者状态。核查至 9 月 27 日 06:26 UTC,Debian将 bookworm 的 6.1.9+dfsg1-0+deb12u1、trixie 的 6.8.7+dfsg1-0+deb13u1 和 forky 的 7.1+dfsg1-1 标为受影响,sid 的 7.1.2+dfsg1-1 已修复。Ubuntu列出的 16.04 至 26.04 六个版本仍为 Needs evaluation。这些状态会变化,实施前应再看所在发行版的记录。直接把 sid 包混入稳定系统,或让上游更新器覆盖包管理器维护的文件,会给后续维护留下新的不确定性。

对于直接使用上游发行包、已有备份和升级流程的站点,可以从后台更新,也可以使用 WP-CLI。先记录核心版本、活动主题、PHP 环境和部署路径,确认代码与数据库备份可恢复,再在同配置副本中检查主题和插件兼容性。下面是管理员操作示例,发行版或托管平台管理的站点应使用各自的更新入口。

wp core version
wp core update --version=7.1.2
wp core version
wp core verify-checksums --version=7.1.2 --include-root

更新后还要确认真正提供请求的所有实例都使用新文件,处理 PHP 工作进程和 OPcache 中可能保留的旧代码,并检查负载均衡后的节点。一个管理容器显示 7.1.2,不能代表每个 Web 实例都已经替换。升级失败时,应继续保持临时限制,恢复到已验证包含本次补丁的部署或分支版本;旧漏洞镜像只能留作隔离调查材料。

如果眼下无法完成更新,我们建议先在反向代理或负载均衡层限制动态 WordPress 请求,必要时只提供静态页面。代价很直接,登录、表单、购物和动态内容可能暂停。这样可以在请求进入 PHP 之前缩小暴露面。仅依靠站内维护插件,需要另外确认它在易受影响流程之前结束请求,不能凭“维护中”的页面外观判断阻断位置。

短期还可在实际 Web PHP 配置中关闭 register_argc_argv,重新加载对应服务并确认生效,切断已公开的 PEAR 参数链。切换到经过验证、没有相关目录布局的主题也可能移除这个入口,但会改变页面模板、组件和站点功能,应先做兼容检查。WAF 规则可以争取更新时间,规则需要同时处理 GET、POST 和多层编码;只拦 URL 中的某个字面字符串,容易遗漏解析后的值。这些是临时处理建议,退出条件是核心补丁部署完成且下面的功能与路径检查通过。

5 验收正常页面,也查修复前留下的文件

修复验收先覆盖站点确实使用的模板。打开普通页面、按 ID 定位的页面、中文名称页面和指定自定义模板的页面,核对布局与内容;使用子主题、嵌套主题目录或区块模板的站点,还要检查它们原有的优先级和回退行为。补丁保留了正常的解码能力和兼容目录,升级后所有非英文页面都失败,显然也需要处理。

维护自定义发行包的团队,可以在隔离副本中加入一个无副作用的目录外测试文件,直接检查模板查找函数的返回值。把包含上级目录组件的候选交给函数时,应拒绝落在允许目录之外的文件;正常的主题内部候选应继续返回预期路径。测试材料只能来自自建目录,不能拿生产配置、系统工具或真实凭据文件充当目标。还应覆盖目标不存在、父主题回退、嵌套主题兼容目录这几种情况,避免只通过一个拒绝用例就结束验收。

wp core verify-checksums可以把核心文件与 WordPress 发布的校验值比较,它在加载 WordPress 之前运行。按所装版本和语言选择正确校验集,--include-root还能报告根目录的额外文件。插件、主题、上传目录、数据库和系统其他位置需要各自的检查;核心文件校验一致,只回答了核心文件这一部分。怀疑主机已经被控制时,先保存证据,在受控副本中使用可信工具检查,避免继续依赖主机上的现有管理程序。

历史排查有现实依据。Patchstack 的当前观测文章记录了 9 月 22 日的探测,并在次日更新中描述了借助 PEAR 尝试写入 PHP 文件的请求;9 月 25 日的 CISA KEV收录确认了已知利用状态。这些材料支持紧急处理,不提供所有 WordPress 站点的受害比例,本文也没有据此估算成功入侵数量。

日志检索可以关注带有 pagename 的异常请求、同时使用 page_id 的查询、编码或重复编码的上级目录组件,以及涉及 pearcmd 的路径。应同时检查代理访问日志和能够合法取得的应用请求记录,留意 POST 参数未必出现在普通访问日志里。保留原始请求值,另做解码副本用于分析,避免覆盖编码层次这一证据。单个 HTTP 200 需要结合响应、文件变化和后续活动判断,边缘拦截日志也应区分请求是否实际到达 PHP。

主机检查重点放在 Web 账号可写的位置、临时目录、主题和插件目录、上传目录,以及意外出现的 PHP 文件。将文件时间、部署记录和请求时间关联起来,再检查新增管理员、计划任务、配置改动及后续出站活动。发现落地文件或其他明确控制迹象时,隔离实例、从可信来源重建代码,并按实际暴露范围更换站点和数据库凭据。删除一个可疑文件后继续运行,无法交代它执行期间做过什么。

评分也需要保留出处。WordPress GHSA 使用 CVSS 4.0 给出 9.2;NVD 截至本次核查处于 Undergoing Analysis,其中 CVSS 3.1 的 8.1 来自 CISA ADP。CNA 原始描述把版本粗略写到 7.1.2 以下,厂商逐分支表列出了更精确的回移修复版本。环境条件、已知利用和修复状态已经足够支持处理顺序,混用不同评分体系或把来源机构写错,只会妨碍读者判断。

本文沿固定源码检查了查询变量、名称整理、模板选择、文件包含及两处补丁,并抽查了三组旧分支回移。没有运行完整 WordPress、主题或 PEAR 利用实验,也没有向任何外部站点发送测试请求。攻击活动来自明确标注的公开观测;实际站点是否走到危险分支、是否已经被写入文件,仍须用该站点的配置和记录回答。

这次问题留给代码审查的一处具体提醒,是在数据改变用途时重新检查它。一个名称经过查询所需的整理,随后又被解码、加上前后缀并交给文件系统,最后还会被 PHP 执行。把每次解释发生的位置连起来,既能看懂为什么主题目录会决定影响,也能知道补丁之后该验证哪个结果。

研究依据

研究依据固定源码与补丁分析,结合厂商公告及公开攻击观测;没有运行 WordPress 利用复现。官方状态核对截至 2026-09-27 06:26 UTC。

来源WordPress、CISA、Patchstack 与发行版记录

证据置信度 高

6证据与来源

6.1时间线

  1. 加入解码后的模板名称

    提交 e7b0581 支持非 ASCII 查询名称,相关分支进入 WordPress 4.7。

  2. 7.1.2 与旧分支补丁发布

    WordPress 公布 GHSA-7hp8-65ch-5whp,修复解码名称校验和模板路径约束。

  3. 公开攻击观测更新

    Patchstack 更新前一日的探测记录,描述尝试借 PEAR 写入 PHP 文件的请求。

  4. CISA 收录已知利用

    CVE-2026-87902 进入 KEV,修复前暴露的站点应同步排查历史影响。

6.2来源与材料

  1. WordPress 官方公告、前提条件与完整分支修复表https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
  2. WordPress 7.1.2 发布说明与维护范围https://wordpress.org/news/2026/09/wordpress-7-1-2-release/
  3. 2016 年模板名称兼容改动https://github.com/WordPress/wordpress-develop/commit/e7b058117ae831898a39517516b11be7d39cf33d
  4. 本次 template.php 修复提交https://github.com/WordPress/wordpress-develop/commit/170944a34b6f4d76decffb5c40b5b068dc7779d8
  5. Patchstack 对探测与文件写入尝试的观测https://patchstack.com/articles/cve-2026-87902-attackers-started-probing-wordpress-sites-hours-after-the-patch/
  6. 加拿大网络安全中心 AV26-952https://www.cyber.gc.ca/en/alerts-advisories/wordpress-security-advisory-av26-952
  7. CISA 已知利用漏洞目录原始数据https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
  8. CNA 与 CISA ADP 的原始 CVE 记录https://cveawg.mitre.org/api/cve/CVE-2026-87902
  9. NVD 当前状态、评分归属与变更历史https://nvd.nist.gov/vuln/detail/CVE-2026-87902
  10. Debian 包修复状态https://security-tracker.debian.org/tracker/CVE-2026-87902
  11. Ubuntu 包评估状态https://ubuntu.com/security/CVE-2026-87902
  12. PHP register_argc_argv 配置语义https://www.php.net/manual/en/ini.core.php#ini.register-argc-argv
  13. WP-CLI 核心校验范围与参数https://developer.wordpress.org/cli/commands/core/verify-checksums/