它在生产环境运行良好。然后我写了测试。

发布: (2026年3月13日 GMT+8 05:01)
7 分钟阅读
原文: Dev.to

Source: Dev.to

应用运行正常。用户可以登录。数据正在保存。没有任何异常。
于是,我自然去寻找问题所在。

任务

我被要求审计一个新的 Agent Referral 功能,位于学校管理系统中——代理推荐学校,获取佣金,诸如此类。没有简要说明。没有检查清单。只要“确保它没问题”。

隐蔽的特权提升

一个辅助方法 resolveAgent() 负责确定当前登录的是哪个代理:

  1. 检查已认证的用户是否为 Agent
  2. 如果不是,检查 URL 是否包含 agent_id 参数,如果有,则从数据库加载对应的代理 ❌

步骤 2 是典型的 特权提升。任何已认证的用户——学校管理员、教师,甚至任何人——都可以在 URL 后追加 ?agent_id=some‑uuid,从而完全访问该代理的仪表板、佣金和付款历史。

修复方案(PHP)

return $user instanceof Agent ? $user : null;

现在你要么是代理 什么也得不到。没有后备方案。也不再信任 URL 中的任何输入。

核心原则:永远不要信任客户端

从浏览器传来的任何东西——URL 参数、表单字段、头信息、Cookie——都可能是伪造的。假设它们都是伪造的。这不是 Laravel 特有的规则;它是互联网的法则,像重力一样不可改变,而且远没有重力那样宽容。

用测试证明修复

我写了测试并运行它们,整个测试套件在任何测试执行之前就炸掉了,因为数据库无法从迁移文件重新构建。

问题 1 – 在 TEXT 列上设置默认值

一个迁移尝试在 TEXT 列上设置默认值。TEXT 是无限长度的,因而 MariaDB 拒绝了:

“I’m not pre‑filling every essay box. That’s expensive and I refuse.”

根据配置,MySQL 可能允许,但 MariaDB 不行。

问题 2 – 复合主键中的可空列

一个复合主键包含了一个可空列。主键如果可以为空,就无法唯一标识记录,所以数据库正确地拒绝了它。

为什么生产环境看起来没问题

生产数据库是经过数月逐步构建且从未被销毁的。我的测试使用了 RefreshDatabase,它会在每次运行时销毁所有内容并重新从头构建。全新的环境暴露了所有悄然累积的捷径。

关注点分离:默认值的归属

把你的后端想象成一家银行:

  • 金库(数据库) – 安全地存储数据。它不进行思考。
  • 柜员(应用程序) – 处理业务逻辑。当没有指定时决定填入什么。

原来的代码让金库去思考;金库拒绝了。我把默认值移到了 模型 层:

protected $attributes = [
    'overall_comment'   => 'This student is good.',
    'principal_comment' => 'This student is hardworking.',
];

现在由应用程序处理逻辑,数据库只负责存储数据。明天把 PostgreSQL 换成 MySQL 不会影响默认值,重写前端也不会影响后端。

测试套件即活文档

我写了八个测试:

  • 5 个安全测试 – 学校管理员尝试各种方式访问代理的仪表盘;每次都返回 401 Unauthorized
  • 3 个认证测试 – 代理仍然可以注册、登录,并在密码错误时被拒绝。入口仍然正常工作。

我修好了”是一种信念。八个绿色勾选是证据。六个月后,当有人问:“我们能确定学校管理员无法进入代理数据吗?”答案不再是 Slack 消息——而是每次代码更改时都会运行的测试套件。

原则 vs. 设计模式

第一原则(物理层面)设计模式(蓝图)
用户输入始终不可信。零信任 → 认证防护,无 URL 参数回退
可能不存在的主键无法标识任何记录。将默认值从数据库移至模型中
数据库无法高效强制执行其未设计的约束。使用工厂和测试来验证行为

模式是工具;原则解释了你选择它的 为什么

对任何系统都要问的三个问题

  1. 谁负责这项责任? 如果答案并不明显,那就是 bug。
  2. 这里的根本真相是什么? 去除假设。
  3. 你能证明吗? 如果不能,你并不知道——只是还没有出错而已。

配方会变,物理规律不会变。

征集故事

留下评论——真的很好奇:你遇到过最糟糕的“它在生产环境下工作”时刻是什么?恐怖越具体越好。👇


后端工程师。我写系统、架构,以及当你去触碰那些本该保持未检查的东西时会发生什么。作品集: cycy.is-a.dev 🚀

0 浏览
Back to Blog

相关文章

阅读更多 »