ThinkPHP6 验证器里我写了 `confirm:password` 却忘了加 `require`,结果空密码直接绕过了重复校验

站长杂谈 23 浏览 0 回复 返回上级

昨晚改注册接口,顺手重构了下验证规则,想着"密码重复校验"这种老需求闭着眼睛都能写。结果测试妹子甩过来一张截图:两个密码框都空着,点提交居然过了,直接进数据库一条空密码记录。

我当时第一反应是前端没做校验,抓包一看,前端明明传了 `password` 和 `repassword` 两个空字符串。问题出在验证器。

错误写法(我手滑提交的版本):

``` protected $rule = [ 'username' => 'require|alphaNum|length:4,16', 'password' => 'require|length:6,32', 'repassword'=> 'confirm:password', // 坑在这儿 'email' => 'require|email', ]; ```

看起来没毛病对吧?`confirm:password` 校验两次输入是否一致。但 ThinkPHP6 的验证逻辑是:如果字段本身没有 `require`,且值为空字符串,大部分校验规则会直接跳过。`confirm` 属于"可选校验"——它默认不强制字段必须存在。

所以 `repassword` 传空字符串时,`confirm:password` 压根没执行,或者执行了但认为"空等于空"也算通过(实际源码里是 `strcmp` 比对,两个空字符串确实相等)。更坑的是,这个空密码进了数据库,后续登录时 `password_verify('' , $hash)` 永远返回 false,用户再也登不上来,还找不到原因。

正确写法(补了 `require` 的版本):

``` protected $rule = [ 'username' => 'require|alphaNum|length:4,16', 'password' => 'require|length:6,32', 'repassword'=> 'require|confirm:password', // 补上 require 'email' => 'require|email', ]; ```

关键区别:`require|confirm:password` 是"先强制存在,再比对一致"。空字符串在 `require` 这层就被拦住了,根本走不到 `confirm`。

我还试过另一种写法,想着更严谨点:

``` 'repassword' => 'requireWith:password|confirm:password' ```

这个意思是"如果 password 存在,则 repassword 必须存在"。看起来聪明,其实更危险——如果前端把 `password` 字段整个删掉不传,`requireWith` 不触发,`confirm` 又不强制,两个校验全跳过。所以注册场景千万别耍小聪明,核心字段直接 `require` 锁死

顺手贴个我现在的习惯:验证器里凡是参与业务主流程的字段,第一条规则必须是 `require`,后面再跟具体校验。`confirm`、`different`、`egt` 这些依赖其他字段的规则,前面必须垫一个 `require` 或者 `requireIf`,不然空值就是定时炸弹。

另外说个副产品:这次事故让我把单元测试里"全空提交"这个 case 优先级调到了最高。以前总觉得测正常流程就够了,现在发现恶意省略字段比传错字段更难防——传错至少会触发校验,不传直接绕过去。

你们有没有被验证器的"隐式跳过"坑过?我查了下文档,ThinkPHP6 这部分行为从 5.1 开始就这样,不算 bug,算特性,但真的很容易忘。

评论0
回复 · 0
还没有回复
微信客服 微信客服