danygiguere/audit-skills
GitHub: danygiguere/audit-skills
一套语言和框架无关的 AI 编程助手审计清单,通过不变式和坏味道检测让 agent 深度审查代码中的安全、正确性和可运维性问题。
Stars: 3 | Forks: 0
# audit-skills
[](LICENSE)
[](https://github.com/danygiguere/audit-skills/tags)
[](https://github.com/danygiguere/audit-skills/actions/workflows/validate.yml)



与语言和框架无关的 AI 编程 agent 审计清单 —
涵盖安全性、正确性和可操作性。适用于 Claude Code、GitHub
Copilot、Cursor、Codex CLI、OpenCode,以及任何能够读取文件的 agent。
每份清单都以**不变式和检测坏味道**的形式编写,而非
框架 API,因此相同的内容可用于审计 Rails 应用、Spring 服务
或 Express API —— 由 agent 提供针对特定框架的转换。
## 演示

*对一个 20 行的交易处理程序执行 `/audit` —— 发现了六个静态分析扫描器
无法察觉的 bug,因为每个 bug 都需要对所有权、并发
和重试进行推理,而不仅仅是模式匹配。每一个都被标记出来,并附带了严重程度和修复建议。*
### 实战示例
在一个约 40 行的 Spring `OrderController` 上运行 `/audit` 会发现 **9
个问题** —— 2 个严重,5 个高危,2 个低危 —— 涵盖了 IDOR、批量赋值、
价格伪造、库存竞态、非原子性扣费、缺少幂等性,以及
N+1 问题。这些问题都无法通过模式匹配发现;每一个都需要对所有权、
并发或重试进行推理。
被审计的代码 —
```
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orders;
private final ProductRepository products;
private final PaymentGateway gateway;
private final CurrentUser currentUser;
public OrderController(OrderRepository orders, ProductRepository products,
PaymentGateway gateway, CurrentUser currentUser) {
this.orders = orders;
this.products = products;
this.gateway = gateway;
this.currentUser = currentUser;
}
@GetMapping("/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orders.findById(orderId).orElseThrow();
}
@GetMapping
public List listOrders() {
List mine = orders.findByUserId(currentUser.id());
List views = new ArrayList<>();
for (Order order : mine) {
List names = new ArrayList<>();
for (OrderLine line : order.getLines()) {
Product p = products.findById(line.getProductId()).orElseThrow();
names.add(p.getName());
}
views.add(new OrderView(order.getId(), order.getTotal(), names));
}
return views;
}
@PostMapping("/checkout")
public Order checkout(@RequestBody Order order) {
BigDecimal total = BigDecimal.ZERO;
for (OrderLine line : order.getLines()) {
total = total.add(line.getUnitPrice().multiply(BigDecimal.valueOf(line.getQuantity())));
}
order.setTotal(total.subtract(order.getDiscount()));
order.setStatus("PAID");
return orders.save(order);
}
@PostMapping
public Order placeOrder(@RequestBody PlaceOrderRequest req) {
Product product = products.findById(req.productId()).orElseThrow();
if (product.getStock() >= req.quantity()) {
product.setStock(product.getStock() - req.quantity());
products.save(product);
BigDecimal amount = product.getPrice().multiply(BigDecimal.valueOf(req.quantity()));
gateway.charge(req.card(), amount);
Order order = new Order();
order.setUserId(currentUser.id());
order.setTotal(amount);
order.setStatus("PAID");
return orders.save(order);
}
throw new IllegalStateException("Out of stock");
}
@PostMapping("/{orderId}/pay")
public ResponseEntity pay(@PathVariable Long orderId, @RequestBody CardDetails card) {
Order order = orders.findById(orderId).orElseThrow();
gateway.charge(card, order.getTotal());
order.setStatus("PAID");
orders.save(order);
return ResponseEntity.ok("charged");
}
}
```
**严重**
**1. 批量赋值 + 价格伪造 + 免费的 "PAID" 订单 — `checkout` (第 45–54 行)**
`@RequestBody Order order` 绑定了整个实体,并且总价是根据**客户端提供的** `unitPrice`、`quantity` 和 `discount` 计算出来的。攻击者可以发送任意他们想要的 `unitPrice`/`discount` —— 包括使 `total` 变为零或负数的值 —— 然后该处理程序会在**从未向支付网关发起扣款的情况下**将状态标记为 `status="PAID"`。绑定的 `userId`/`id` 也允许将订单归属于任何人(所有权重定向)。
*不变式:* 批量赋值(允许列表)+ 授权。*修复:* 仅解码包含 `{productId, quantity}` 的 DTO;在服务器端通过 `Product.getPrice()` 计算价格;设置 `userId = currentUser.id()`;绝不信任客户端的价格/折扣/状态。
**2. IDOR — 支付任何订单,按其总额扣款 — `pay` (第 76–83 行)**
`orders.findById(orderId)` 没有所有权检查,接着执行 `gateway.charge(card, order.getTotal())` 和 `setStatus("PAID")`。任何通过身份验证的用户都可以提供任何 `orderId` 并篡改另一个主体的订单。`orderId` 是一个自增的 `Long` 类型,因此目标是可枚举的。
*不变式:* IDOR(对其他主体数据的写入访问权限)。*修复:* `orders.findByIdAndUserId(orderId, currentUser.id())`;如果不存在则返回 404。
**高危**
**3. IDOR — 读取任何订单 — `getOrder` (第 25–28 行)**
`orders.findById(orderId).orElseThrow()` 会向任何调用者返回任何订单,没有所有权范围限制。连续的 ID 使其可被枚举;返回的是原始的 `Order` 实体(也存在数据过度暴露)。
*修复:* 将查找范围限定在 `currentUser.id()`;返回一个视图 DTO,而不是实体。
**4. 丢失更新 / 超卖库存(先检查后执行竞态)— `placeOrder` (第 60–62 行)**
`if (stock >= qty)` 然后执行 `setStock(stock - qty)` 再 `save` —— 两个并发请求都通过了检查并导致超卖。没有锁、版本号或原子递减。
*不变式:* 状态管理。*修复:* 原子性条件更新(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`,检查受影响的行数)或使用 `@Lock(PESSIMISTIC_WRITE)`。
**5. 非原子的库存 + 扣款 + 订单 — `placeOrder` (第 61–71 行)**
三个步骤没有事务,且中间还穿插了一个外部调用:保存库存 → `gateway.charge` → 保存订单。如果扣款抛出异常,库存已经扣除但没有生成订单;如果扣款成功后 `orders.save` 失败,客户会被扣款但没有订单。
*不变式:* 原子性。*修复:* 将数据库的持久化写入放在同一个 `@Transactional` 单元中;在提交后扣款(或在失败时进行补偿性退款)。
**6. 重试/重复提交导致的重复扣款(无幂等性)— `placeOrder` (第 65 行) 和 `pay` (第 79 行)**
两者都调用 `gateway.charge` 且没有幂等键,也没有已处理状态的防护。客户端重试、双击或代理重放都会导致扣款两次(在 `placeOrder` 中,还会导致库存递减两次)。`pay` 还会对已经 `PAID` 的订单重新扣款 —— 因为没有 `status != "PAID"` 的防护。
*不变式:* 幂等性。*修复:* 传递 Stripe 风格的幂等键;在 `pay` 中,如果已经 `PAID` 则不执行任何操作(返回 200)。
**7. 缺少输入验证导致金额为负 — `placeOrder` / `checkout`**
对 `quantity` 没有限制。在 `placeOrder` 中,负的 `quantity` 会通过 `stock >= qty` 的检查,*增加* 库存(`stock - (−n)`),并且会扣除一个**负数**金额(实际上相当于对攻击者的卡进行了退款)。
*不变式:* API 契约与验证。*修复:* 在数量上使用带 `@Positive` 的 `@Valid`;在扣款前拒绝非正数金额。
**低危**
**8. `orElseThrow()` → 返回 500 而不是 404 — 第 27, 37, 58, 78 行**
缺失的订单/产品会引发 `NoSuchElementException`,表现为 500 错误而不是 404。
*不变式:* 异常处理(状态映射)。*修复:* `orElseThrow(() -> new ResponseStatusException(NOT_FOUND))`。
**9. N+1 查询 — `listOrders` (第 34–39 行)**
`products.findById` 在遍历用户订单的循环内部为每个订单明细运行一次 —— 产生 N×M 次查询。
*不变式:* N+1。*修复:* 收集所有的 `productId` 并一次性执行 `findAllById`,或者在 repository 查询中使用 join。
## 包含内容
- `AGENTS.md` — 所有 30 个不变式的单页摘要;将其内容复制
到你项目的 `AGENTS.md` 中,这样每个 agent 都能将其作为上下文。
- `.agents/skills/audit/` — 路由 skill,包含所有 30 份清单和
打包在 `references/` 下的修复模式(四个类别:访问与数据安全、输入/API、正确性、可操作性)。
- `.agents/skills/audit-*` — 轻量级的按主题划分的包装 skill,因此每个清单
都可以单独调用(`/audit-idor`、`/audit-injection`、
`/audit-fix-authz`,……)。该软件包安装的所有内容都以
`audit` 开头,因此它会在你的其他 skill 中保持分组状态。
## 审计项
`/audit` 运行完整的审计 —— 它会识别代码的功能,并
应用下面所有匹配的清单。每个主题也都可以单独调用
(点击查看清单内容)。
### 访问与数据安全
| 审计项 | 检查内容 |
|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`/audit-authorization`](.agents/skills/audit/references/access-data-security/authorization.md) | 在执行操作时的服务端权限检查 —— 权限提升、仅 UI 的权限控制、只检查读取而不检查修改 |
| [`/audit-authn-session`](.agents/skills/audit/references/access-data-security/authn-session.md) | 登录、注销和重置流程 —— 会话固定、账户枚举、token 过期与一次性使用、记住我存储 |
| [`/audit-idor`](.agents/skills/audit/references/access-data-security/idor.md) | 在没有验证请求者权限的情况下,通过请求提供的 ID 获取或修改资源 |
| [`/audit-data-exposure`](.agents/skills/audit/references/access-data-security/data-exposure.md) | 过度暴露的响应、错误和日志 —— 整个模型的序列化、堆栈跟踪、PII |
| [`/audit-crypto`](.agents/skills/audit/references/access-data-security/crypto-data-protection.md) | 密码哈希、token 随机性、常数时间比较、自制加密、密钥处理 |
| [`/audit-output-encoding`](.agents/skills/audit/references/access-data-security/output-encoding.md) | XSS —— 在没有适当上下文编码的情况下,将用户数据渲染到 HTML、JS、CSS、URL、headers 或电子邮件中 |
| [`/audit-tenant-isolation`](.agents/skills/audit/references/access-data-security/tenant-isolation.md) | 跨租户泄漏 —— 未加作用域的查询、无租户标识的缓存键、跨越租户的后台任务 |
| [`/audit-csrf`](.agents/skills/audit/references/access-data-security/csrf.md) | 在使用 cookie/session 认证的状态变更端点上,没有 CSRF token 或 origin 验证 |
| [`/audit-mass-assignment`](.agents/skills/audit/references/access-data-security/mass-assignment.md) | 请求 payload 被整体绑定到模型上 —— 可写的角色/所有者/余额字段,使用了黑名单而不是允许列表 |
### 输入与 API
| 审计项 | 检查内容 |
|------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`/audit-injection`](.agents/skills/audit/references/input-api-dependency/injection.md) | SQL/NoSQL、命令、模板和路径注入 —— 将输入拼接到查询、shell 或模板中 |
| [`/audit-config`](.agents/skills/audit/references/input-api-dependency/config.md) | 不安全的配置 —— 生产环境开启 debug、宽松的 CORS、缺失的安全 headers、cookie 标志 |
| [`/audit-secrets`](.agents/skills/audit/references/input-api-dependency/secrets.md) | 硬编码的凭证、日志或版本控制中的机密、权限过大的密钥、没有轮换路径 |
| [`/audit-api-validation`](.agents/skills/audit/references/input-api-dependency/api-contract-validation.md) | 边界验证 —— 类型、边界、允许的字段、信任客户端计算的值(如价格或角色) |
| [`/audit-file-handling`](.agents/skills/audit/references/input-api-dependency/file-handling.md) | 路径遍历、未验证的上传、缺少大小限制、从 web root 提供的文件、zip-slip |
| [`/audit-ssrf`](.agents/skills/audit/references/input-api-dependency/ssrf.md) | 指向受用户影响 URL 的服务器端请求 —— 允许列表、私有 IP 范围、重定向重新验证;包括开放重定向 |
| [`/audit-parser-differentials`](.agents/skills/audit/references/input-api-dependency/parser-differentials.md) | 验证器接受但消费者读取方式不同的输入 —— 未锚定的正则表达式、startswith 允许列表、双重 URL 解析器、先验证后重新解析 |
### 正确性
| 审计项 | 检查内容 |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| [`/audit-atomicity`](.agents/skills/audit/references/correctness/atomicity.md) | 没有事务的多重存储写入 —— 故障中幸存的局部状态 |
| [`/audit-idempotency`](.agents/skills/audit/references/correctness/idempotency.md) | 运行两次时会表现异常的处理程序 —— webhook、支付、队列重投递、重复提交 |
| [`/audit-background-work`](.agents/skills/audit/references/correctness/background-work.md) | 任务和消费者 —— 无限重试、有害消息、缺失超时、重复或乱序交付 |
| [`/audit-state-management`](.agents/skills/audit/references/correctness/state-management.md) | 竞态条件 —— 在锁、原子原语或约束的情况下对共享状态执行先检查后操作 |
| [`/audit-exception-handling`](.agents/skills/audit/references/correctness/exception-handling.md) | 被吞掉的错误、一网打尽式的捕获、丢失错误原因、缺少清理工作,以及错误的 HTTP 状态码 (404 vs 403, 401, 422, 409) |
| [`/audit-discarded-async`](.agents/skills/audit/references/correctness/discarded-async.md) | 触发即忘的 bug —— 创建但从未被 await、return 或组合的 promise、future 或响应式 publisher;直接 subscribe;静默且从未执行的冷写入 |
| [`/audit-cardinality`](.agents/skills/audit/references/correctness/cardinality.md) | 假设查询只匹配一行的操作 —— 在非唯一列上执行 UPDATE/DELETE 导致扩散,在非唯一字段上使用 findOne/.single(),在没有数据库约束的情况下将列视为唯一的 |
### 可操作性
| 审计项 | 检查内容 |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| [`/audit-nplus1`](.agents/skills/audit/references/operability/nplus1.md) | 在遍历集合的循环内执行的查询 —— 或 HTTP/缓存调用 |
| [`/audit-observability`](.agents/skills/audit/references/operability/observability.md) | 静默失败 —— 被吞掉的错误、没有标识符的日志、没有指标或告警路径 |
| [`/audit-migration-safety`](.agents/skills/audit/references/operability/migration-safety.md) | 锁表的 schema 变更、没有采用扩展-收缩模式的破坏性变更、未分批次的回填 |
| [`/audit-resource-limits`](.agents/skills/audit/references/operability/resource-limits.md) | 由输入导致的无限工作 —— 缺少分页、大小上限、速率限制、灾难性的正则 |
| [`/audit-blocking-io-async`](.agents/skills/audit/references/operability/blocking-io-async.md) | 在事件循环或协程上的阻塞调用、在调度器上执行的 CPU 密集型工作、sync-over-async、缺少超时 |
| [`/audit-schema-design`](.agents/skills/audit/references/operability/schema-design.md) | 在 FK 列和热路径上缺少索引、仅存在于 ORM 中而没有真实外键的关系、默认的 ON DELETE、仅存在于应用代码中的完整性规则、浮点数表示金钱 |
| [`/audit-statelessness`](.agents/skills/audit/references/operability/statelessness.md) | 在加入第二个副本或部署时会中断的状态 —— 内存中的 session 和计数器、静态可变状态、本地磁盘上传、进程内的锁和调度器 |
### 修复
| Skill | 应用 |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`/audit-fix-authz`](.agents/skills/audit/references/remediation/authz-patterns.md) | 针对授权、IDOR 和租户隔离问题的修复模式 —— 作用域查询、策略对象、默认拒绝 |
| [`/audit-fix-async`](.agents/skills/audit/references/remediation/async-patterns.md) | 针对正确性问题的修复模式 —— 事务、发件箱模式、幂等键、锁、有限重试 |
| [`/audit-fix-observability`](.agents/skills/audit/references/remediation/observability-patterns.md) | 针对可观测性缺失的修复模式 —— 结构化日志、关联 ID、RED 指标、基于症状的告警 |
## 安装
将 `.agents` 文件夹复制到你的项目中 —— 这就是安装的全部内容
(它只是 markdown;不会执行任何内容):
```
git clone --depth 1 https://github.com/danygiguere/audit-skills /tmp/audit-skills && cp -R /tmp/audit-skills/.agents your-project/
```
**Cursor** 也可以直接通过 repo 链接安装,如果你使用
[skills CLI](https://github.com/vercel-labs/skills):
`npx skills add danygiguere/audit-skills --all`。
## 添加到你的 AGENTS.md 中
本 repo 的 [`AGENTS.md`](AGENTS.md) 是所有 30 个
不变式的单页摘要。将其内容复制到你项目的 `AGENTS.md` 中(如果你
已经有一个,请将其追加进去 —— 绝不要替换你自己的):合并到那里后,它能让每个 agent 在每次提示时都
对不变式保持潜在的感知;如果没有它,skill 只会在
被触发时激活。它的路由表指向
已安装的 skill 文件夹。
**Claude Code 注意事项:** Claude Code 尚未读取 `.agents/skills/`
([anthropics/claude-code#31005](https://github.com/anthropics/claude-code/issues/31005))。
使用以下命令进行桥接:
```
mkdir -p .claude && ln -s ../.agents/skills .claude/skills
echo '@AGENTS.md' > CLAUDE.md # if you don't already have a CLAUDE.md
```
## 替代方案:将你的项目放到这里
与其将 skill 复制到每个项目中,不如克隆一次
`audit-skills`,然后将你的项目放入 `projects/` 文件夹内 —
它已被 gitignored,因此你的代码永远不会出现在 `git status` 中,并且执行
`git pull`(或 `git checkout vX.Y`)可以在不触碰你放在那里的任何内容的情况下更新
skill。
```
git clone https://github.com/danygiguere/audit-skills
# 将你想 audit 的任何 project 放入 projects/ 目录中
cp -R /path/to/myproject audit-skills/projects/myproject
```
然后从该 repo 内部进行审计:
```
/audit projects/myproject
```
保持最新:`git pull` —— 你的项目不会受到任何影响。
当你想要审计一个你不拥有或不想
修改的 repo 时,或者当你更愿意维护一个集中的 skill 副本
而不是每个项目维护一个时,这非常有用。
## 使用
- **自动** —— 让你的 agent “审查这个端点” / “审计这个
diff”;skill 会根据它们的描述自动触发。
- **通过命令** —— 使用 `/audit` 进行完整审计,或者按主题进行:
`/audit-idor`、`/audit-injection`、`/audit-atomicity`,…… 所有这些
默认都会审计你当前的 diff;指定一个文件、文件夹或分支可以
审计其他内容。
- **通过名称** —— “在这个 webhook 处理程序上运行幂等性检查清单”。
- **修复** —— 在确认问题之后:`/audit-fix-authz`、
`/audit-fix-async`、`/audit-fix-observability`(见“修复是如何工作的”)。
## 修复是如何工作的
审计和修复被刻意设为独立的步骤。`/audit` 和
`audit-*` 清单只负责**查找并报告** —— 它们绝不会修改代码。
修复发生在你提出要求时:在报告后说“修复它们”,或者运行
`audit-fix-*` 命令。
每个发现的问题都有对应的修复方案;区别在于它们存放的位置:
**大多数主题 —— 修复就在清单本身中。** 每份清单的
`Example` 部分展示了漏洞形式及其修复后的形式。对于
注入、机密、输出编码或 N+1 查询等主题,修复是
机械性的,并且只有一个正确答案(参数化查询、将机密
移至环境变量、在循环前批量加载)。当你说“修复它”时,
agent 会应用该修复后的形式 —— 无需额外的命令。
**八个主题 —— 修复是一种架构选择。** 有些发现的问题有
几种有效的修复方案,并带有实际的权衡(一个幂等性 bug:去重表、
幂等键、UPSERT,还是绝对状态写入?)。这些主题指向
一份修复指南,引导 agent 进行选择:
| 问题来源 | 指南 | 命令 |
|-----------------------------------------------------------|-----------------------------------------|----------------------------|
| authorization, IDOR, tenant isolation | `remediation/authz-patterns.md` | `/audit-fix-authz` |
| atomicity, idempotency, background work, state management | `remediation/async-patterns.md` | `/audit-fix-async` |
| observability | `remediation/observability-patterns.md` | `/audit-fix-observability` |
无论哪种方式,流程都是相同的:**审计 → 确认的问题 → 要求
修复。** 修复在任何地方都遵循相同的规则:以恢复
不变式的最小改动为准,匹配周围的代码风格,并提供证明
修复有效的测试 —— 绝不与无关的重构混在一起。
## 版本控制
规范的版本位于 [`VERSION`](VERSION) 中。它被标记到
伴随你的项目的两个工件上:`audit` skill(其 frontmatter 中的 `version:`
字段加上一个源页脚)和 `AGENTS.md` 摘要
(页脚)。因此,已安装的副本总是会说明它们的版本以及
来源 —— 比较你的标记与本 repo 的 `VERSION` 就可以
知道你是否已经过时。(无需将 `VERSION` 复制到你的
项目中 —— 标记会随工件一起传递。)你的 agent 可以替你完成:“检查我的
audit-skills 是否为最新版本”就能为它提供所需的一切。
## License
[MIT](LICENSE)
被审计的代码 — OrderController.java
```
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orders;
private final ProductRepository products;
private final PaymentGateway gateway;
private final CurrentUser currentUser;
public OrderController(OrderRepository orders, ProductRepository products,
PaymentGateway gateway, CurrentUser currentUser) {
this.orders = orders;
this.products = products;
this.gateway = gateway;
this.currentUser = currentUser;
}
@GetMapping("/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orders.findById(orderId).orElseThrow();
}
@GetMapping
public List/audit 的报告内容 — 9 个问题
**严重**
**1. 批量赋值 + 价格伪造 + 免费的 "PAID" 订单 — `checkout` (第 45–54 行)**
`@RequestBody Order order` 绑定了整个实体,并且总价是根据**客户端提供的** `unitPrice`、`quantity` 和 `discount` 计算出来的。攻击者可以发送任意他们想要的 `unitPrice`/`discount` —— 包括使 `total` 变为零或负数的值 —— 然后该处理程序会在**从未向支付网关发起扣款的情况下**将状态标记为 `status="PAID"`。绑定的 `userId`/`id` 也允许将订单归属于任何人(所有权重定向)。
*不变式:* 批量赋值(允许列表)+ 授权。*修复:* 仅解码包含 `{productId, quantity}` 的 DTO;在服务器端通过 `Product.getPrice()` 计算价格;设置 `userId = currentUser.id()`;绝不信任客户端的价格/折扣/状态。
**2. IDOR — 支付任何订单,按其总额扣款 — `pay` (第 76–83 行)**
`orders.findById(orderId)` 没有所有权检查,接着执行 `gateway.charge(card, order.getTotal())` 和 `setStatus("PAID")`。任何通过身份验证的用户都可以提供任何 `orderId` 并篡改另一个主体的订单。`orderId` 是一个自增的 `Long` 类型,因此目标是可枚举的。
*不变式:* IDOR(对其他主体数据的写入访问权限)。*修复:* `orders.findByIdAndUserId(orderId, currentUser.id())`;如果不存在则返回 404。
**高危**
**3. IDOR — 读取任何订单 — `getOrder` (第 25–28 行)**
`orders.findById(orderId).orElseThrow()` 会向任何调用者返回任何订单,没有所有权范围限制。连续的 ID 使其可被枚举;返回的是原始的 `Order` 实体(也存在数据过度暴露)。
*修复:* 将查找范围限定在 `currentUser.id()`;返回一个视图 DTO,而不是实体。
**4. 丢失更新 / 超卖库存(先检查后执行竞态)— `placeOrder` (第 60–62 行)**
`if (stock >= qty)` 然后执行 `setStock(stock - qty)` 再 `save` —— 两个并发请求都通过了检查并导致超卖。没有锁、版本号或原子递减。
*不变式:* 状态管理。*修复:* 原子性条件更新(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`,检查受影响的行数)或使用 `@Lock(PESSIMISTIC_WRITE)`。
**5. 非原子的库存 + 扣款 + 订单 — `placeOrder` (第 61–71 行)**
三个步骤没有事务,且中间还穿插了一个外部调用:保存库存 → `gateway.charge` → 保存订单。如果扣款抛出异常,库存已经扣除但没有生成订单;如果扣款成功后 `orders.save` 失败,客户会被扣款但没有订单。
*不变式:* 原子性。*修复:* 将数据库的持久化写入放在同一个 `@Transactional` 单元中;在提交后扣款(或在失败时进行补偿性退款)。
**6. 重试/重复提交导致的重复扣款(无幂等性)— `placeOrder` (第 65 行) 和 `pay` (第 79 行)**
两者都调用 `gateway.charge` 且没有幂等键,也没有已处理状态的防护。客户端重试、双击或代理重放都会导致扣款两次(在 `placeOrder` 中,还会导致库存递减两次)。`pay` 还会对已经 `PAID` 的订单重新扣款 —— 因为没有 `status != "PAID"` 的防护。
*不变式:* 幂等性。*修复:* 传递 Stripe 风格的幂等键;在 `pay` 中,如果已经 `PAID` 则不执行任何操作(返回 200)。
**7. 缺少输入验证导致金额为负 — `placeOrder` / `checkout`**
对 `quantity` 没有限制。在 `placeOrder` 中,负的 `quantity` 会通过 `stock >= qty` 的检查,*增加* 库存(`stock - (−n)`),并且会扣除一个**负数**金额(实际上相当于对攻击者的卡进行了退款)。
*不变式:* API 契约与验证。*修复:* 在数量上使用带 `@Positive` 的 `@Valid`;在扣款前拒绝非正数金额。
**低危**
**8. `orElseThrow()` → 返回 500 而不是 404 — 第 27, 37, 58, 78 行**
缺失的订单/产品会引发 `NoSuchElementException`,表现为 500 错误而不是 404。
*不变式:* 异常处理(状态映射)。*修复:* `orElseThrow(() -> new ResponseStatusException(NOT_FOUND))`。
**9. N+1 查询 — `listOrders` (第 34–39 行)**
`products.findById` 在遍历用户订单的循环内部为每个订单明细运行一次 —— 产生 N×M 次查询。
*不变式:* N+1。*修复:* 收集所有的 `productId` 并一次性执行 `findAllById`,或者在 repository 查询中使用 join。
标签:AI代码助手, SOC Prime, 开发工具, 提示词工程, 策略决策点, 软件质量, 防御加固