![图片[1]-xboard空验证码绕过漏洞如何修复?从源码校验到 Cloudflare、宝塔 WAF 与 1Panel 防护实践](https://zwn.cc/wp-content/uploads/2026/05/20ba5eb2f720260523122640.webp)
在安全问题里,有一类漏洞看起来并不复杂,甚至有些“低级”:用户提交了一个空参数,缓存里刚好也没有值,于是系统误以为验证通过。
但真正值得警惕的是,这类问题往往出现在登录、注册、找回密码、邮箱验证码校验等关键链路上。一旦身份验证逻辑被绕过,后果可能不是某个页面报错,而是账号被接管、密码被重置,甚至后台权限被非法获取。
本文围绕一种典型场景展开:由于输入未校验与缓存未命中返回空值共同导致的验证码绕过漏洞。我们会从漏洞成因、源码修复原则、Cloudflare Snippets 虚拟补丁、宝塔 WAF 规则,以及 1Panel 免费版 WAF 的限制与替代方案几个角度,系统梳理该如何真正防住这类问题。
一、漏洞的本质:空输入遇上空缓存
很多验证码验证逻辑的基本流程是:
- 用户提交邮箱验证码;
- 后端从缓存中读取该邮箱对应的验证码;
- 将用户输入与缓存值进行比较;
- 如果一致,则认为验证通过。
看起来没问题,但风险通常藏在边界条件里。
例如下面这类伪代码:
$user_code = $request->get('email_code'); // 攻击者传入空字符串
$cache_code = Cache::get($email); // 缓存失效或不存在,返回 null 或空字符串
if ($user_code != $cache_code) {
return "验证码错误";
}
// 验证通过,继续执行登录、重置密码等操作
如果用户传入的 email_code 是空字符串 "",而缓存中的验证码因为过期、失效或不存在,也返回了 null、false 或空字符串,那么在某些弱类型比较或不严谨逻辑下,就可能出现错误判断。
尤其在 PHP、JavaScript 等语言中,如果开发者使用了松散比较,或者没有显式处理 null、空字符串、数组等异常输入,就可能导致:
用户没有提交有效验证码,但系统却认为验证码校验通过。
这类漏洞并不依赖复杂的攻击技巧,核心问题是:系统把“没有值”和“验证正确”混淆了。
二、根本修复原则:先校验输入,再校验缓存,最后严格比对
要真正修复这类漏洞,不能只依赖外部拦截规则。最可靠的方式,是从后端源码层面重构验证逻辑。
一个安全的验证码校验流程,应当遵循三个原则:
1. 先校验用户输入是否合法
在进入业务逻辑之前,必须明确判断用户提交的验证码是否存在、是否为字符串、是否为空、长度是否符合预期。
例如:
email_code不能为空;- 不能是数组;
- 不能是
null; - 不能只是空格;
- 长度应符合系统验证码规则,例如 4 位、6 位或其他固定格式。
2. 再校验缓存中的验证码是否有效
缓存未命中时,不能继续比较。
很多漏洞的根源就在于:开发者直接把 Cache::get() 的结果拿来参与比较,却没有区分“验证码不存在”和“验证码错误”。
正确做法是:
- 如果缓存返回
null,说明验证码不存在或已过期; - 如果缓存返回空字符串,也应视为异常状态;
- 此时应直接拒绝请求,而不是继续进入比对逻辑。
3. 最后使用严格相等进行比对
不要使用松散比较。
在 PHP 中应使用 !== 或 ===;在 JavaScript 中同样应使用 !== 或 ===。
严格比较不仅比较值,还比较类型,能避免很多弱类型转换带来的安全隐患。
三、推荐的源码修复示例
下面是一个相对安全的 PHP 风格示例,核心思想是:输入、缓存、比对三个环节都不能偷懒。
// 1. 接收并严格校验用户输入
$email_code = $request->input('email_code');
if (!is_string($email_code) || trim($email_code) === '') {
return response()->json([
'message' => '验证码格式不正确或不能为空'
], 400);
}
// 2. 从缓存中获取正确的验证码
$cached_code = Cache::get('email_code_' . $email);
// 3. 显式校验缓存是否有效
if (is_null($cached_code) || $cached_code === '') {
return response()->json([
'message' => '验证码已过期或不存在'
], 400);
}
// 4. 严格比对类型与内容
if ($email_code !== $cached_code) {
return response()->json([
'message' => '验证码错误'
], 400);
}
// 5. 验证成功后,继续后续业务,并立即作废验证码
Cache::forget('email_code_' . $email);
这段逻辑里最关键的不是代码本身,而是顺序:
先拒绝异常输入,再拒绝失效缓存,最后才进行严格比较。
这也是防御性编程中非常重要的一点:不要假设用户输入是正常的,也不要假设缓存一定存在。
四、Cloudflare Snippets:可以作为临时虚拟补丁,但不能替代源码修复
如果系统已经暴露在公网,并且短时间内无法立即修改源码,使用 Cloudflare Snippets 做边缘拦截是一种有效的应急措施。
它的价值在于:请求还没到达源站,就先在边缘节点被过滤掉。
一个设计较好的 Snippets 方案,通常会覆盖以下情况:
application/json请求体中的空值,例如"email_code":"";- JSON 中的
null,例如"email_code":null; - 表单提交中的空值,例如
email_code=&; - 参数在请求体末尾,例如
email_code=; - 数组形态绕过,例如
email_code[]; - 不同
Content-Type的变体提交。
这类方案的优点很明显:
- 部署快;
- 不需要立即改动后端代码;
- 可以在攻击发生时快速降低风险;
- 对常见异常请求有较好的拦截效果。
但它也有边界。
Cloudflare 边缘防护的局限
Cloudflare Snippets 本质上是边缘网络层的虚拟补丁。它不是业务逻辑本身的一部分。
如果出现以下情况,防护可能失效:
- 源站 IP 暴露,攻击者绕过 CDN 直连源站;
- 某些接口没有经过 Cloudflare 代理;
- 项目后续新增了验证码接口,但没有更新 Snippets 路径;
- 域名切换或代理配置变更;
- 请求编码、分块传输等方式造成边缘规则漏判。
因此,Cloudflare Snippets 更适合作为应急响应:
它可以帮你争取修复时间,但不能代替后端代码的正确性。
五、路径覆盖:很多人会忽略的防护细节
无论是 Cloudflare、宝塔 WAF,还是 Nginx 规则,只要你是基于路径做拦截,就必须确认路径覆盖完整。
例如,涉及邮箱验证码的接口可能不止一个:
- 注册接口;
- 找回密码接口;
- 邮箱登录接口;
- 免密登录接口;
- 后台管理员登录接口;
- 后台管理员重置密码接口;
/api/v1/...;/api/v2/...;- 自定义后台路径;
- 插件或扩展模块提供的认证接口。
如果只保护了一个显眼的接口,而遗漏了另一个旧接口、备用接口或后台接口,攻击者仍然可能从遗漏路径绕过。
所以,在部署任何外部规则前,建议先做一次接口梳理:
哪些接口接收 email_code?
哪些接口依赖缓存验证码?
哪些接口可以触发登录、注册、改密或绑定邮箱?
哪些接口存在旧版本或隐藏路径?
安全防护最怕“只修了自己看见的地方”。
六、宝塔 WAF 的应急配置思路
如果你的站点运行在宝塔环境中,可以借助宝塔 WAF 的自定义规则、POST 过滤或参数过滤做临时拦截。
核心目标是:仅针对特定认证接口,拦截 email_code 为空或类型异常的 POST 请求。
推荐方式:使用 POST 参数过滤
如果宝塔防火墙支持按指定 URL 匹配 POST 参数,可以这样配置:
- 规则名称:拦截空验证码
- 匹配方向:POST 参数
- 参数名:
email_code - 匹配模式:正则匹配
- 匹配内容:
Regex^$
- URI 条件:仅匹配认证相关路径,例如:
Regex.*/api/v1/passport/auth/.*
- 动作:阻断或返回 403
这种方式比较精准,因为它只针对特定接口的特定参数生效,不容易误伤其他业务。
使用请求体正则进行拦截
如果无法按参数过滤,只能匹配整个请求体,可以考虑类似规则:
Regex(?:"email_code"\s*:\s*""|"email_code"\s*:\s*null|email_code=(?:&|$)|email_code\[\])
它可以覆盖几类常见异常形态:
"email_code":"""email_code":nullemail_code=&email_code=email_code[]
不过,使用请求体正则时务必结合 URI 条件。
不要设置全局拦截所有 email_code 为空的请求,否则容易误伤正常业务。
例如,某些前端页面初始化时可能会提交空字段,如果全局拦截,可能会造成表单保存失败、页面异常或接口不可用。
七、1Panel面板WAF 的限制与替代方案
1Panel 的开源免费版 WAF 在匹配对象上相对精简,通常仅支持:
- URL;
- IP;
- Header;
- Host;
- 请求类型。
它并不直接支持对请求体 Body 的深度解析和匹配。
这意味着,如果攻击请求的特征藏在请求体里,例如:
Json{ "email_code": ""}
免费版 WAF 很可能无法直接识别并拦截。
面对这个限制,有两个思路。
方案一:提高高危接口的访问门槛
既然无法精确识别空验证码,就退一步,对高危路径进行访问控制和限速。
例如针对:
Text/api/v1/passport/auth/
增加以下规则:
- URL 包含
/api/v1/passport/auth/; - 请求类型为
POST; - 配合 CC 防护或速率限制;
- 限制单 IP 在短时间内对该接口的请求频率。
这种方式不能从根本上修复漏洞,但可以提高攻击成本,降低批量扫描、批量探测和自动化攻击的效率。
适合在无法修改源码、WAF 又不支持 Body 匹配时作为临时缓解。
方案二:在 Nginx 层面做请求体过滤
如果 1Panel WAF 本身无法匹配 Body,可以考虑直接在 Nginx 或 OpenResty 层面对特定路径做过滤。
示例思路如下:
location ~* /api/v1/passport/auth/ {
client_max_body_size 10m;
if ($request_body ~* '("email_code"\s*:\s*""|"email_code"\s*:\s*null|email_code=(&|$)|email_code\[\])') {
return 403 '{"message":"Invalid request body: email_code cannot be empty."}';
}
# 保留原有 proxy_pass 或 fastcgi_pass 配置
}
需要注意的是,Nginx 中的 $request_body 并不总是可靠可用。部分场景下,如果请求体没有被完整读取,该变量可能为空,导致规则不生效。
因此,Nginx 过滤仍然属于临时方案。
真正稳妥的方式,依然是在后端业务逻辑中加入明确的输入校验和缓存校验。
八、为什么不建议只靠 WAF?
WAF、CDN 边缘规则、Nginx 过滤都有价值,尤其在应急响应中,它们可以快速降低风险。
但它们不应该成为最终答案。
原因很简单:
1. WAF 看到的是流量,不是业务语义
WAF 可以判断请求里是否出现某些特征,但它并不真正理解业务。
它不知道:
- 哪个验证码对应哪个邮箱;
- 缓存是否存在;
- 该接口是否用于改密;
- 当前用户是否处在合法流程中;
- 某个空字段是否一定意味着攻击。
业务安全问题最终还是要回到业务代码中解决。
2. 外部规则容易遗漏路径
只要新增接口、调整路由、切换版本,就可能产生遗漏。
而源码修复一旦落在统一的验证码验证函数或服务中,覆盖面会更可靠。
3. WAF 存在绕过可能
高级攻击者可能尝试通过编码、分块传输、Content-Type 变形、参数污染等方式绕过规则。
如果后端代码本身安全,即使 WAF 漏掉了请求,也不会造成验证绕过。
这才是最重要的安全边界。
九、推荐的修复优先级
如果你正在处理类似漏洞,我建议按照以下优先级推进:
第一优先级:修复后端源码
这是最终解决方案。
重点包括:
- 校验
email_code必须存在; - 校验
email_code必须为字符串; - 去除首尾空格后不能为空;
- 校验验证码长度和格式;
- 缓存未命中时直接拒绝;
- 使用严格比较;
- 验证成功后立即删除验证码;
- 将验证码校验逻辑封装成统一方法,避免多个接口各写一套。
第二优先级:升级官方修复版本
如果你使用的是第三方系统或开源项目,优先检查官方是否已经修复该问题。
如果官方发布了补丁或新版,应尽快升级。
源码本地热修虽然有效,但后续维护成本较高,升级官方版本通常更稳妥。
第三优先级:部署临时虚拟补丁
在源码修复前,可以通过以下方式降低风险:
- Cloudflare Snippets;
- 宝塔 WAF 自定义规则;
- Nginx 请求体过滤;
- 高危接口限速;
- 禁止源站 IP 直连;
- 临时关闭存在风险的接口;
- 加强登录、找回密码相关日志审计。
第四优先级:持续观察日志
规则上线后,不要默认万事大吉。
建议观察:
- 是否有大量 403;
- 是否存在正常用户被误拦截;
- 是否有攻击者切换路径或 Content-Type;
- 是否出现源站直连请求;
- 是否有异常找回密码、邮箱登录、后台登录记录。
安全修复不是“写一条规则”就结束,而是一个验证和迭代的过程。
十、一个更稳健的验证码验证设计
如果要从工程角度做得更好,可以把验证码验证封装成统一服务,而不是在每个接口里单独写判断。
例如:
class EmailCodeVerifier
{
public function verify(string $email, $inputCode): bool
{
if (!is_string($inputCode) || trim($inputCode) === '') {
return false;
}
if (!preg_match('/^\d{6}$/', $inputCode)) {
return false;
}
$cacheKey = 'email_code_' . $email;
$cachedCode = Cache::get($cacheKey);
if (!is_string($cachedCode) || $cachedCode === '') {
return false;
}
if ($inputCode !== $cachedCode) {
return false;
}
Cache::forget($cacheKey);
return true;
}
}
这样做有几个好处:
- 所有验证码校验都走统一入口;
- 后续修改规则时只需要改一处;
- 更容易做单元测试;
- 更容易审计;
- 不同业务接口不容易出现“有的修了,有的没修”的问题。
在安全设计里,统一入口非常重要。分散的逻辑越多,遗漏的概率就越高。
真正的安全感来自代码本身
空验证码绕过漏洞的本质,并不是某个 WAF 规则没写好,也不是某个缓存组件返回了空值,而是后端验证逻辑没有把异常状态处理清楚。
这类问题的修复核心可以概括为一句话:
不要让空输入和空缓存参与身份验证判断。
Cloudflare Snippets、宝塔 WAF、Nginx 过滤、1Panel 限速,这些方案都可以作为应急手段,尤其在无法立即发版时,它们能够帮助系统快速降低暴露风险。
但最终,真正可靠的修复仍然应该落在源码层面:
- 输入必须校验;
- 缓存未命中必须拒绝;
- 比对必须严格;
- 验证成功后必须作废验证码;
- 验证逻辑最好统一封装。
安全不是靠某一层“挡一下”就够了。边缘防护可以帮你争取时间,而业务代码的正确性,才是系统最后也是最关键的防线。








![表情[doge]-造物ZAOWU](https://zwn.cc/wp-content/themes/zibll/img/smilies/doge.gif)
![表情[xieyanxiao]-造物ZAOWU](https://zwn.cc/wp-content/themes/zibll/img/smilies/xieyanxiao.gif)
![表情[touxiao]-造物ZAOWU](https://zwn.cc/wp-content/themes/zibll/img/smilies/touxiao.gif)
暂无评论内容