2. 安全编码威胁建模实战
威胁建模不是安全团队的专属仪式,而是开发者在写第一行代码之前就应该完成的思维演练。它的核心价值在于:把"可能被怎么攻击"这个模糊问题,转化为"代码里必须写哪些校验、哪些限制、哪些拒绝逻辑"的具体清单。本节以一个真实的业务系统——在线订单与支付平台——为对象,完整走一遍从资产识别、信任边界划分、攻击面枚举,到 STRIDE 威胁清单输出,再到编码安全需求落地的全过程。
一、先确定建模对象与资产
假设我们面对的是一个典型的电商订单系统,它包含 Web 前端、订单服务、支付服务、库存服务、消息队列和 MySQL 数据库,并对外接入第三方支付网关。威胁建模的第一步不是画攻击者,而是回答"我们有什么值得被保护"。资产识别要落到具体的数据和组件上,而不是笼统的"用户信息"。
可以按数据资产与组件资产两类梳理。数据资产包括:用户账号与密码哈希、收货地址与手机号、订单金额与状态、支付流水号与交易凭证、库存扣减记录。组件资产包括:订单服务 API、支付回调接口、库存扣减接口、消息队列 Topic、数据库连接凭据、第三方支付网关的商户密钥。每一项资产都要标注它的安全属性——是关注机密性(如手机号)、完整性(如订单金额),还是可用性(如库存服务)。
二、划分信任边界,标出数据跨越的位置
信任边界是威胁建模中最容易被忽略、却最能定位问题的一步。它的定义是:数据或控制流从一个信任级别进入另一个信任级别的位置。跨越边界的地方,就是必须做校验、鉴权和审计的地方。
在本系统中,至少存在四条关键信任边界:浏览器到 Web 前端(不可信到可信)、Web 前端到订单服务(外部到内部)、订单服务到第三方支付网关(内部到外部)、支付网关回调到支付服务(外部到内部)。每一条边界上,都要问三个问题:谁在跨越?携带什么数据?如果数据被篡改会怎样?
三、枚举攻击面,用 STRIDE 逐项提问
攻击面是攻击者能够与系统交互的所有入口。在本系统中,攻击面包括:订单创建 API 的请求参数、支付回调 URL、库存扣减接口、消息队列的消费入口、数据库连接配置、前端页面中的隐藏字段。注意,前端隐藏字段(如订单金额)虽然用户看不见,但完全可以被篡改,因此它属于攻击面而非安全控制。
STRIDE 是六类威胁的缩写,它提供了一套系统化的提问框架:Spoofing(仿冒)、Tampering(篡改)、Repudiation(抵赖)、Information Disclosure(信息泄露)、Denial of Service(拒绝服务)、Elevation of Privilege(权限提升)。对每一条信任边界和每一个攻击面,逐项套用这六类问题,就能得到一份结构化的威胁清单。
以"支付回调接口"为例:攻击者能否伪造回调(Spoofing)?回调中的订单金额能否被篡改(Tampering)?用户能否否认已支付(Repudiation)?回调响应中是否泄露内部订单信息(Information Disclosure)?大量伪造回调是否导致服务不可用(Denial of Service)?回调能否触发越权的订单状态变更(Elevation of Privilege)?六个问题问完,威胁清单自然成形。
四、把威胁转化为编码安全需求
威胁清单本身不是交付物,可落地的编码安全需求才是。转化的关键是:每一条威胁都要对应到具体的代码位置、校验逻辑和失败处理方式。下面给出本系统部分威胁到编码需求的转化示例。
| 威胁(STRIDE) | 攻击面 | 编码安全需求 |
|---|---|---|
| 伪造支付回调(S) | 支付回调 URL | 回调必须校验第三方签名,签名密钥从环境变量读取,禁止硬编码;校验失败返回 403 并记录审计日志 |
| 篡改订单金额(T) | 订单创建 API | 金额以服务端商品价格重新计算,忽略客户端传入的 amount 字段;使用 BigDecimal 避免浮点误差 |
| 库存超卖(T) | 库存扣减接口 | 扣减使用带条件的原子更新:UPDATE stock SET qty=qty-? WHERE sku=? AND qty>=?,影响行数为 0 时回滚 |
| 消息重放(S/T) | 消息队列消费入口 | 每条订单事件携带唯一 eventId,消费端用 Redis SETNX 做幂等去重,TTL 设为 24 小时 |
| 越权改单(E) | 订单状态接口 | 每次状态变更前校验 order.userId == currentUser.id,且只允许合法状态迁移(如待支付→已支付) |
| 凭据泄露(I) | 数据库配置 | 数据库密码通过密钥管理服务注入,代码仓库中不得出现明文;日志脱敏手机号与卡号 |
五、让需求可验证:从"应该做"到"能测出"
编码安全需求如果只写成"要做签名校验",开发完成后仍然无法判断是否真正落地。因此每条需求都应附带可验证的判据。例如"回调必须校验签名"可以细化为:构造一个签名错误的回调请求,接口应返回 403 且订单状态不变;构造一个签名正确但金额被篡改的回调,接口应拒绝并记录告警。这样的判据可以直接写成单元测试或集成测试用例。
六、实战中的常见偏差
第一类偏差是只画架构图不标信任边界。架构图展示组件连接,信任边界展示"哪里必须设防",两者不能互相替代。第二类偏差是威胁清单停留在"存在 SQL 注入风险"这种层面,没有定位到具体接口和参数。第三类偏差是安全需求写成原则而非约束,例如"应保证数据安全",这种表述无法指导编码,也无法验证。
正确的做法是:每个威胁都绑定到具体的攻击面,每条编码需求都绑定到具体的代码位置和失败处理方式,每个失败处理方式都能被测试用例触发。以本系统为例,订单创建接口的金额校验、支付回调的签名验证、库存扣减的原子条件更新、消息消费的幂等去重,这四项构成了最核心的编码安全基线。它们不是抽象原则,而是可以直接写进代码评审清单的检查项。
威胁建模的最终目的,是让开发者在写代码时清楚地知道:这个参数来自不可信边界,必须校验;这个状态变更涉及权限,必须鉴权;这个操作可能被重放,必须幂等。当这些判断成为编码习惯,安全就不再是事后补丁,而是代码本身的结构。