一个搜索框拿管理员:CraftCMS未认证SQL注入漏洞复盘,CVSS9.8全版本在射程"

admin 2026-11-10 16:27:52 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

一个搜索框拿管理员:Craft CMS未认证SQL注入漏洞复盘,CVSS 9.8全版本在射程

零日手记
零日手记

随笔漫记安全路

2026年11月10日 12:38
北京

去阅读

2026年11月9日,GitHub Advisory Database上出现一条critical级别记录:Craft CMS全版本未认证SQL注入,CVSS 9.8,可直接提权管理员。影响4.0以来所有版本,补丁9月中旬就已发布,但大量站点至今未更新。

这个漏洞值得拆开看,因为它踩中了一个经典陷阱:PDO emulated prepares + sprintf拼接SQL = 堆叠注入。

漏洞档案

项目 详情
CVE CVE-2026-115934
GHSA GHSA-qcvp-ggx3-97r6
CVSS 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWE CWE-89 SQL注入
影响版本 Craft CMS 4.0.0–4.18.8 / 5.0.0–5.11.1
补丁版本 4.18.9 / 5.11.2(2026-09-17发布)
前置条件 前端使用了craft.search搜索功能 + MySQL/MariaDB后端
认证要求 无需认证

攻击链:从搜索框到管理员

Craft CMS的前台搜索功能通过craft.search处理用户输入,搜索词经Search::normalizeSearchQuery()解析后,最终流入Search::_sqlWhere()方法拼成SQL WHERE子句。

问题出在_sqlWhere()——它对列名$key做了quoteColumnName()转义,但值$val直接用sprintf拼进SQL字符串:

// 漏洞代码 (src/services/Search.php)
private function _sqlWhere(string $key, string $oper, string|int $val): string
{
    $key = Craft::$app()->getDb()->quoteColumnName($key);
    // ↑ $key做了转义
    // ↓ $val直接sprintf拼接,没有quoteValue!
    return sprintf(\"(%s %s '%s')\", $key, $oper, $val);
}

$val被单引号包裹后直接拼入SQL。攻击者在搜索参数中闭合单引号,就能注入任意SQL。

关键放大器:PDO emulated prepares。Craft CMS的MySQL连接默认开启PDO::ATTR_EMULATE_PREPARES,这意味着PDO在客户端模拟预处理,不在MySQL服务端执行真正的参数化查询。后果是堆叠查询(stacked queries)可以执行——攻击者用分号串联第二条SQL语句,实现写操作。

攻击者只需在前台搜索URL中构造[attribute]参数:

GET /actions/search/search?q=test&[attribute]=1';UPDATE users SET admin=1 WHERE id=1;--

三条路径全部走通:

步骤 操作 结果
1 闭合单引号+分号 脱离原查询上下文
2 堆叠UPDATE users SET admin=1 WHERE id=1 user ID 1的admin标志位置1
3 Craft CMS权限检查短路 admin列=1即跳过所有权限校验 ,无需改密码

同一个写原语还能覆盖任意账户的密码哈希。报告者原话:\"The write primitive was used to set the admin flag on user ID 1 in the users table, confirmed by reading the value back.\"

补丁:三处修复

补丁commit 8e3023c55e改了三个文件:

修复1:_sqlWhere()对值做quoteValue()

// 修复后
private function _sqlWhere(string $key, string $oper, string|int $val): string
{
    $db = Craft::$app()->getDb();
    $key = $db->quoteColumnName($key);
    $val = $db->quoteValue($val);  // ← 新增:值转义
    return \"($key $oper $val)\";    // ← 不再手动包单引号
}

核心变化:$val从sprintf裸拼接改为quoteValue()转义,同时去掉外层手动单引号(quoteValue()自带引号包裹)。

修复2:SearchQuery构造函数白名单defaultTermOptions

// 修复后:只允许这4个key
private const ALLOWED_DEFAULT_TERM_OPTIONS = ['subLeft', 'subRight', 'exclude', 'exact'];

$this->_defaultTermOptions = array_intersect_key(
    $defaultTermOptions,
    array_flip(self::ALLOWED_DEFAULT_TERM_OPTIONS),
) + [...defaults...];

防止攻击者通过defaultTermOptions注入非预期参数。

修复3:ElementHelper::cleanseQueryCriteria()校验search类型

// 修复后:search必须是字符串
if (isset($criteria['search']) && !is_string($criteria['search'])) {
    unset($criteria['search']);
}

防止数组类型绕过。

时间线

日期 事件
2026-09-10 研究者通过Hackrate平台提交漏洞
2026-09-11 Craft团队确认接受,创建GHSA
2026-09-15 修复commit提交(8e3023c55e)
2026-09-17 补丁版本4.18.9 / 5.11.2发布
2026-10-19 安全公告公开
2026-11-09 严重级别从High升级Critical(CVSS 9.8)
2026-11-10 GitHub Advisory Database收录

临时缓解

无法立即升级的站点,在Web服务器层拦截含[attribute]或%5Battribute%5D的查询字符串:

# Nginx
if ($query_string ~* \"(%5Battribute%5D|\\[attribute\\])\") {
    return 403;
}
# Apache
RewriteEngine On
RewriteCond %{QUERY_STRING} (.*(\\[attribute\\]|%5Battribute%5D).*)
RewriteRule .* - [F]

注意:这只是止血,根本方案是升级到4.18.9+/5.11.2+。

两个教训

教训一:sprintf拼SQL永远是隐患。 这个函数对列名做了quoteColumnName()却漏了值的quoteValue()——典型的\"修了一半\"。PHP生态里sprintf+SQL的组合是高危模式,即使你手动包了单引号,一旦值内含单引号就绕过。正确做法:要么全程参数化查询,要么对所有动态部分调用对应的quote方法。

教训二:PDO emulated prepares是堆叠注入的温床。 当PDO::ATTR_EMULATE_PREPARES=true(MySQL默认),PDO在客户端拼接SQL字符串再整体发送给服务端,分号分隔的多条语句可以一起执行。真正的服务端预处理不支持堆叠查询。Craft CMS + MySQL的组合默认就开着这个选项,把一个本应只能读取的注入点升级成了读写原语。

检查你的PHP应用:

// 关闭emulated prepares(推荐)
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

这会让PDO使用真正的服务端预处理,堆叠查询将被拒绝。注意少数旧代码可能依赖emulated prepares的行为,切换前做回归测试。


参考:

  • GitHub Advisory: GHSA-qcvp-ggx3-97r6 / CVE-2026-115934
  • 补丁commit: craftcms/cms@8e3023c55e
  • 漏洞报告: Hackrate HCKRT-Y64NY8
  • 补丁版本: 4.18.9 / 5.11.2

天融信可信数据空间解决方案获IDC推荐" 网络安全文章

天融信可信数据空间解决方案获IDC推荐"

天融信的可信数据空间解决方案获IDC推荐,该方案通过体系化架构和多维安全能力融合,支持数据安全流通与价值释放,解决数据协作、资源协同、使用管控、审计追溯等挑战,实现数据可用不可见、可控可追溯的流通要求。
评论:0   参与:  0