# 骨架代码里的 placeholder fallback / skeleton auth 是生产环境真风险，必须硬失败

> **日期**: 2026-05-14
> **类型**: 安全/prod-readiness 反模式
> **来源**: pre-push AI 自检 (`/tmp/ai-review-local.json`) 抓到 internal-app-platform Phase 0 三处问题

## 现象（本次抓到的 3 个并列实例）

1. **env-crypto.service.ts**：`INTERNAL_APP_ENV_MASTER_KEY` 缺失时只 `logger.warn` 然后用 `'placeholder-not-for-prod'` 派生密钥继续加密。结果：prod 配错时数据被用假密钥加密入库，**事后无法解密、对外看起来工作正常**——不可逆数据损失。
2. **internal-app-platform.controller.ts**：`req.user` 占位 + 注释「Entra middleware 在 Phase 0 实现期接入」。结果：如果忘了接 middleware 就推 UAT，攻击者构造请求即可冒充任意员工。
3. **webhook.controller.ts**：签名校验失败时返 `{ statusCode: 401 }` JSON body **但 HTTP 真实是 200**（被 `@HttpCode(HttpStatus.OK)` 钉死）。结果：Gitea 看 2xx 不重试，监控看 2xx 不告警，攻击者拿到 200 不知道被拒。

## 共性根因

**骨架/占位代码**为了"先让流程跑通"，把 fail 路径降级成 warn + 假数据。这在以下条件叠加时变成真事故：

1. 占位上线前需要"接入真版本"才安全
2. 接入工作流没有强制 gate（没人会忘）
3. 静默降级路径在 prod 与正常路径在外观上无法区分

只要任一环节漏接，prod 就在"看起来正常"的状态下处于不安全状态。

## 解决（已采纳的统一模式）

**生产硬失败 + 非生产显式拒绝服务 + env 闸开关默认 off。** 三件事一起做：

1. **构造期判定 + prod 抛错**（不在请求路径上做，避免每请求都判）
   ```ts
   constructor(private config: ConfigService) {
     const masterKey = this.config.get<string>('INTERNAL_APP_ENV_MASTER_KEY');
     this.keyMissing = !masterKey || masterKey === '__GENERATE_RANDOM__';
     if (this.keyMissing && process.env.NODE_ENV === 'production') {
       throw new Error('INTERNAL_APP_ENV_MASTER_KEY missing');
     }
   }
   ```
2. **业务入口 assert + 非生产也拒服务**（即使 dev 也别让假密钥写入 DB）
   ```ts
   encrypt(plaintext: string) {
     this.assertKeyConfigured();  // throws if keyMissing
     ...
   }
   ```
3. **HTTP 边界用 framework exception，不要返 JSON body 假装错**
   - 错的：`return { statusCode: 401, ... }` ← 真 HTTP 仍是 200
   - 对的：`throw new UnauthorizedException(...)` ← 真 401
4. **骨架鉴权需 env flag 显式开启，默认 off**
   ```ts
   private guardSkeletonAuth() {
     if (process.env.INTERNAL_APP_ENABLE_SKELETON_AUTH === 'true') return;
     throw new Error('Entra middleware 未挂；UAT/prod 必须先接入');
   }
   ```
   `.env.example` 默认 `false`，本地调试自行翻 `true`。

## 适用面

任何「骨架 → 真实接入」的过渡期代码（auth、加密、第三方 API 适配、token 校验、签名校验）：

- 在 PR 描述 / 代码注释里写"待接入 X"**不够**——人会忘
- 必须让代码本身在没接入时**不可用**（构造期抛 / 业务入口拒服务 / env flag 默认 off）
- 区分清楚 prod-only 硬失败（如加密密钥）vs 全环境拒服务（如 controller 占位鉴权）

## 反模式 checklist（每次写"占位"前自查）

- [ ] 占位 fallback 是否会在 prod 默默把假数据写入 DB？→ 必须 prod throw
- [ ] HTTP error 是否真返 4xx/5xx？还是 `200 + { ok: false }`？→ webhook / 外部回调走真状态码
- [ ] 占位 controller 是否需要专门 env flag 才能启用？→ 默认 off
- [ ] CI 是否会在 .env.example 缺这个 flag 时报红？→ 把它加进 .env.example

## 状态

- [x] env-crypto prod throw + non-prod assertKeyConfigured
- [x] InternalAppPlatformController guardSkeletonAuth (4 端点)
- [x] webhook signature → UnauthorizedException
- [x] webhook 业务失败 → ForbiddenException / NotFoundException / HttpException
- [x] INTERNAL_APP_ENABLE_SKELETON_AUTH 默认 false
- [ ] 看团队是否要把"骨架代码 prod 硬失败"提升到 `docs/standards/`（出现 3+ 次后再升级）
