业务流程缺陷攻防
"流程"是所有业务系统的骨架:订单从"待支付"到"已完成"需要经过支付、发货、收货;请假申请需要经过直属上级审批、部门经理审批;退款需要经过审核、打款。流程出漏洞,攻击者就能跳过审批直接拿结果。
一、什么是状态机
几乎所有业务实体都可以抽象成状态机:
订单状态机:
待支付 ──支付成功──▶ 待发货 ──发货──▶ 已发货 ──确认收货──▶ 已完成
│ │
└──取消──▶ 已取消 └──申请退款──▶ 退款中 ──退款成功──▶ 已退款
请假审批状态机:
待提交 ──提交──▶ 待直属上级审批 ──批准──▶ 待部门经理审批 ──批准──▶ 已通过
│ │
└──驳回──▶ 草稿 └──驳回──▶ 待直属上级审批
漏洞根源:攻击者不通过标准的"触发事件"来转换状态,而是直接修改状态字段。
二、漏洞一:直接修改状态字段
2.1 漏洞描述
后端允许前端直接指定目标状态,没有校验状态转换合法性。
2.2 漏洞代码(请假审批)
// VULNERABLE: 允许前端直接传任意 targetStatus
app.post('/api/approval/submit', authMiddleware, async (req, res) => {
const { applicationId, targetStatus } = req.body;
const application = await db.Application.findOne({ where: { id: applicationId } });
if (!application) return res.status(404).json({ error: '申请不存在' });
// ❌ 攻击者可以直接把状态改成 APPROVED
// 正常需要经过两步审批,现在一步到位
await application.update({ status: targetStatus });
res.json({ success: true, newStatus: targetStatus });
});
2.3 攻击
POST /api/approval/submit
Authorization: Bearer USER_TOKEN
{
"applicationId": 42,
"targetStatus": "APPROVED" // ❌ 跳过了所有审批步骤!
}
2.4 修复:状态转换表(State Transition Table)
// FIXED: 严格定义允许的状态转换
const APPROVAL_FLOW = {
PENDING_SUBMIT: {
submit: 'PENDING_SUPERVISOR',
},
PENDING_SUPERVISOR: {
approve: 'PENDING_MANAGER',
reject: 'REJECTED',
},
PENDING_MANAGER: {
approve: 'APPROVED',
reject: 'REJECTED',
},
REJECTED: {
resubmit: 'PENDING_SUPERVISOR', // 被驳回后可以重新提交
},
APPROVED: {}, // 终态,不允许任何转换
};
// 每个转换还需要检查角色权限
const ACTION_ROLES = {
submit: ['EMPLOYEE'],
approve_supervisor: ['SUPERVISOR'],
approve_manager: ['MANAGER'],
reject: ['SUPERVISOR', 'MANAGER'],
};
app.post('/api/approval/action', authMiddleware, async (req, res) => {
const { applicationId, action } = req.body; // ✅ 传 action(动作),不传 targetStatus
const application = await db.Application.findOne({
where: { id: applicationId, applicantId: req.user.id }
});
if (!application) return res.status(404).json({ error: '申请不存在' });
const currentStatus = application.status;
const allowedTransitions = APPROVAL_FLOW[currentStatus];
if (!allowedTransitions || !allowedTransitions[action]) {
return res.status(400).json({ error: `当前状态 ${currentStatus} 不允许 ${action}` });
}
// ✅ 角色校验
const requiredRoles = getActionRoles(action, currentStatus);
if (!requiredRoles.includes(req.user.role)) {
return res.status(403).json({ error: '无权执行此操作' });
}
// ✅ 执行转换
const newStatus = allowedTransitions[action];
await db.sequelize.transaction(async (t) => {
await application.update({
status: newStatus,
updatedAt: new Date(),
actionLog: db.sequelize.literal(
`JSON_ARRAY_APPEND(action_log, '$', JSON_OBJECT('action', '${action}', 'actor', ${req.user.id}, 'time', NOW()))`
),
}, { transaction: t });
// 通知下一个审批人
await notifyNextApprover(application, newStatus, t);
});
res.json({ success: true, newStatus });
});
function getActionRoles(action, currentStatus) {
if (action === 'approve') {
if (currentStatus === 'PENDING_SUPERVISOR') return ['SUPERVISOR'];
if (currentStatus === 'PENDING_MANAGER') return ['MANAGER'];
}
return ACTION_ROLES[action] || [];
}
三、漏洞二:订单状态跳过
3.1 漏洞描述
订单状态应该是"待支付 → 待发货 → 已发货 → 已完成",但攻击者可以直接标记"已完成"来触发退款或奖励。
3.2 漏洞代码
// VULNERABLE: 普通用户可以调用任意状态转换接口
// 虽然接口名叫 /api/order/confirm,但没有校验调用者角色
app.post('/api/order/confirm', authMiddleware, async (req, res) => {
const { orderId } = req.body;
const order = await db.Order.findOne({ where: { id: orderId } });
// ❌ 没有检查 order.status 是否真的是"已发货"
// ❌ 没有检查 req.user.id === order.userId
// ❌ 任何已登录用户都能调用
if (order.status !== 'SHIPPED') {
return res.status(400).json({ error: '订单未发货' });
}
await order.update({ status: 'COMPLETED', completedAt: new Date() });
// 触发积分发放
await db.User.increment('points', { by: order.totalAmount, where: { id: order.userId } });
res.json({ success: true });
});
3.3 变种漏洞:先发货再取消
// 另一个漏洞:状态转换没有互斥
// 攻击者: 发货 → 确认收货 → 取消订单
// 正常流程: 发货 → 收货后就不能取消了
// 修复思路:状态转换必须有完整的前置条件检查
const ORDER_FLOW = {
PENDING_PAYMENT: { pay: 'PAID', cancel: 'CANCELLED' },
PAID: { ship: 'SHIPPED', refund: 'REFUNDING' },
SHIPPED: { confirm: 'COMPLETED', refund: 'REFUNDING' },
COMPLETED: { refund: 'REFUNDING' },
REFUNDING: { refundSuccess: 'REFUNDED', refundReject: 'PAID' },
CANCELLED: {},
REFUNDED: {},
};
function canTransition(fromStatus, action) {
return ORDER_FLOW[fromStatus] && ORDER_FLOW[fromStatus][action];
}
四、漏洞三:角色绕过审批
4.1 漏洞描述
"我请假需要直属上级审批,但我的直属上级是我自己(我设置的汇报对象是我自己)"——业务逻辑上的死胡同。
4.2 常见问题
- 审批人未设置 → 跳过审批直接通过
- 审批人是申请人自己 → 自审自批
- 审批人离职了 → 流程卡住或被自动跳过
- 多级审批只校验最后一级 → 前面几级形同虚设
4.3 漏洞代码
// VULNERABLE: 审批链构造错误
app.post('/api/leave/submit', authMiddleware, async (req, res) => {
const { leaveDays, reason } = req.body;
const leave = await db.Leave.create({
userId: req.user.id,
days: leaveDays,
reason,
status: 'PENDING',
});
// ❌ 只查了 CEO 审批,中间级别的 supervisor 没加
// 或者 supervisor 字段是可选的,没填就跳过
const ceo = await db.User.findOne({ where: { role: 'CEO' } });
await db.LeaveApprover.create({
leaveId: leave.id,
approverId: ceo.id,
order: 1,
});
res.json({ success: true });
});
4.4 修复:完整的审批链构造
// FIXED: 完整多级审批 + 自审批检查
async function buildApprovalChain(userId, type) {
const chain = [];
const employee = await db.User.findOne({ where: { id: userId } });
const seenApprovers = new Set([userId]); // ✅ 申请人自己加入黑名单
let current = employee;
let depth = 0;
const MAX_DEPTH = 10;
while (current && depth < MAX_DEPTH) {
// 找到当前人的直接上级
const manager = await db.User.findOne({
where: { departmentId: current.departmentId, level: current.level - 1 },
});
if (!manager) break; // 已经到顶了
// ✅ 自审批检查:如果上级是自己或已经在审批链里,标记异常
if (seenApprovers.has(manager.id)) {
throw new Error('审批链异常:发现循环或自审批');
}
seenApprovers.add(manager.id);
// 根据请假类型决定审批级别
if (type === 'LEAVE' && manager.level >= 2) {
chain.push({ approverId: manager.id, level: manager.level, role: 'MANAGER' });
} else if (type === 'EXPENSE' && manager.level >= 3) {
chain.push({ approverId: manager.id, level: manager.level, role: 'FINANCE_MANAGER' });
} else {
chain.push({ approverId: manager.id, level: manager.level, role: 'APPROVER' });
}
current = manager;
depth++;
}
// 至少要有一级审批
if (chain.length === 0) {
throw new Error('无法构造审批链,请检查组织架构');
}
return chain;
}
app.post('/api/leave/submit', authMiddleware, async (req, res) => {
const { leaveDays, reason } = req.body;
await db.sequelize.transaction(async (t) => {
const chain = await buildApprovalChain(req.user.id, 'LEAVE');
const leave = await db.Leave.create({
userId: req.user.id,
days: leaveDays,
reason,
status: `PENDING_APPROVAL_1`,
totalApprovals: chain.length,
}, { transaction: t });
for (let i = 0; i < chain.length; i++) {
await db.LeaveApprover.create({
leaveId: leave.id,
approverId: chain[i].approverId,
order: i + 1,
role: chain[i].role,
status: i === 0 ? 'PENDING' : 'WAITING', // 只有第一个是 PENDING,后面的等待
}, { transaction: t });
}
});
res.json({ success: true, message: '已提交,等待审批' });
});
五、漏洞四:并发导致的状态混乱
5.1 场景一:退款与发货并发
时间线:
t1: 用户申请退款,状态 → REFUNDING
t2: 管理员点击发货,没有检查最新状态(读到的还是 PAID)
t3: 发货成功,发了货
t4: 退款成功,钱退了
结果: 用户既拿到了货,又退了钱
5.2 场景二:审批并发
t1: 审批人A 看到状态 PENDING_SUPERVISOR
t2: 审批人B 也看到状态 PENDING_SUPERVISOR
t3: A 批准 → 状态变成 PENDING_MANAGER
t4: B 也批准 → 状态又变?取决于代码怎么写
5.3 修复:乐观锁 / 条件更新
// 用 version 字段做乐观锁
app.post('/api/order/ship', authMiddleware, async (req, res) => {
const { orderId } = req.body;
const [affectedCount, updatedRows] = await db.Order.update(
{
status: 'SHIPPED',
shippedAt: new Date(),
version: db.sequelize.literal('version + 1'), // 乐观锁
},
{
where: {
id: orderId,
status: 'PAID', // ✅ 只有 PAID 才能发货
version: req.body.version, // ✅ 前端传来的版本号必须匹配
},
}
);
if (affectedCount === 0) {
// 要么状态已经变了(被退款了),要么版本号不匹配
const order = await db.Order.findOne({ where: { id: orderId } });
return res.status(400).json({
error: `订单当前状态 ${order.status},无法发货`,
});
}
// 发货逻辑...
res.json({ success: true });
});
// 审批场景:条件更新确保只有一个审批人能推进状态
async function approveApplication(applicationId, approverId) {
const result = await db.sequelize.transaction(async (t) => {
const record = await db.ApprovalRecord.findOne({
where: { applicationId, approverId, status: 'PENDING' },
transaction: t,
lock: t.LOCK.UPDATE, // ✅ 行级锁,防止并发审批
});
if (!record) {
throw new Error('不是当前审批人或已处理');
}
// 更新当前审批记录
await record.update({ status: 'APPROVED', decidedAt: new Date() }, { transaction: t });
// 推进到下一个审批人
const nextRecord = await db.ApprovalRecord.findOne({
where: { applicationId, order: record.order + 1 },
transaction: t,
});
if (nextRecord) {
await nextRecord.update({ status: 'PENDING' }, { transaction: t });
await db.Application.update(
{ status: `PENDING_APPROVAL_${nextRecord.order}` },
{ where: { id: applicationId }, transaction: t }
);
} else {
// 最后一级审批通过
await db.Application.update(
{ status: 'APPROVED', approvedAt: new Date() },
{ where: { id: applicationId }, transaction: t }
);
}
});
return result;
}
六、漏洞五:撤销 / 撤回绕过
6.1 场景:请假已审批通过,但申请人撤回了
// VULNERABLE: 撤回接口没有限制终态
app.post('/api/leave/withdraw', authMiddleware, async (req, res) => {
const { leaveId } = req.body;
const leave = await db.Leave.findOne({ where: { id: leaveId, userId: req.user.id } });
// ❌ 什么状态都能撤回,包括已经审批通过的
await leave.update({ status: 'WITHDRAWN' });
// 更糟:如果系统自动销假,撤回后又可以重新请
res.json({ success: true });
});
// FIXED: 只有特定状态才能撤回
const WITHDRAW_ALLOWED_STATUS = ['PENDING_APPROVAL_1', 'PENDING_APPROVAL_2'];
app.post('/api/leave/withdraw', authMiddleware, async (req, res) => {
const { leaveId } = req.body;
const leave = await db.Leave.findOne({ where: { id: leaveId, userId: req.user.id } });
if (!leave) return res.status(404).json({ error: '申请不存在' });
if (!WITHDRAW_ALLOWED_STATUS.includes(leave.status)) {
return res.status(400).json({
error: `当前状态 ${leave.status} 不允许撤回`,
});
}
await leave.update({ status: 'WITHDRAWN' });
res.json({ success: true });
});
七、安全流程设计模式汇总
7.1 状态机引擎(推荐)
class StateMachine {
constructor(transitions) {
this.transitions = transitions;
}
canTransition(fromState, event) {
return this.transitions[fromState]?.[event] !== undefined;
}
transition(obj, event, actor) {
const current = obj.status;
const allowed = this.transitions[current];
if (!allowed || !allowed[event]) {
throw new Error(`状态 ${current} 不允许事件 ${event}`);
}
const next = allowed[event];
obj.status = next;
obj.updatedAt = new Date();
obj.lastAction = { event, actor, at: new Date() };
return next;
}
}
// 使用:订单状态机
const orderMachine = new StateMachine({
PENDING_PAYMENT: { pay: 'PAID', cancel: 'CANCELLED' },
PAID: { ship: 'SHIPPED', refund: 'REFUNDING' },
SHIPPED: { confirm: 'COMPLETED', refund: 'REFUNDING' },
COMPLETED: { refund: 'REFUNDING' },
REFUNDING: { refundSuccess: 'REFUNDED', refundReject: (from) => from === 'REFUNDING' ? 'PAID' : from },
CANCELLED: {},
REFUNDED: {},
});
// 用法:不让前端传 targetStatus,只传 action
const nextStatus = orderMachine.transition(order, 'ship', req.user.id);
// 自动校验:PAID → SHIPPED ✅
// 自动拒绝:COMPLETED → ship ❌
7.2 通用防御原则
| 原则 | 实现 |
|---|---|
| 不让前端传目标状态 | 前端只传 action(动作),后端查表得到目标状态 |
| 状态转换表 | 每种状态明确定义允许的事件和目标状态 |
| 角色-动作绑定 | 每种动作限制特定角色才能执行 |
| 乐观锁 / 行级锁 | 并发操作时保证只有一个成功 |
| 终态不可变 | APPROVED、COMPLETED 等终态不允许任何转换 |
| 审批链完整校验 | 防止自审批、循环审批、跳过中间级别 |
| 动作留痕 | 每次状态变更记录完整的 action log |
| 回滚前置检查 | 退款/撤回等反向操作必须检查前置条件 |
八、总结
业务流程漏洞的本质是:开发者把状态当成了一个可以自由赋值的字段,而不是一个受约束的状态机节点。
修复流程漏洞的正确思路不是到处加 if 判断,而是引入显式的状态机模型——在系统层面定义清楚"从什么状态、允许什么动作、到什么状态、谁能做"。
业务逻辑安全的三个层次:
- 数据层:CHECK 约束、外键、唯一索引
- 逻辑层:状态机引擎、转换表、乐观锁
- 角色层:RBAC + 动作绑定,谁能做什么事
把这三层搭起来,大部分流程漏洞就自然消失了。