# 纵深防御与最小权限

> 前面每一层都会被突破——这一章讲怎么让「被突破」不等于「全线崩溃」：爆炸半径怎么算、失败要往安全的方向倒、以及最小权限在数据库、云、网络三处的具体落法。

- 作者：David（道雾轩）
- 专栏：网站安全一本通（https://daiw.net/manual/web-security.md）
- 最后更新：2026-08-11
- 原文：https://daiw.net/manual/web-security/defense-in-depth
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 纵深防御与最小权限

前面十几章讲的每一道防线，都会在某个时刻被突破。这一章的主题是：**那之后会发生什么。**

## 从「会不会被打穿」到「打穿之后呢」

问错问题的样子：

> 「我们的登录接口安全吗？」

问对问题的样子：

> 「假设攻击者已经拿到了一个普通用户的会话，他接下来能做什么？」
> 「假设应用被 RCE 了，攻击者手上的凭据能碰到哪些系统？」
> 「假设数据库被拖走了，里面的数据泄露出去有多严重？」

**这三个「假设」就是纵深防御的思考方式。** 每一层都要在「上一层已经失守」的前提下设计。

## 爆炸半径

给你的每个组件算一笔账：**它被完全控制之后，能波及多远？**

| 场景 | 小爆炸半径 | 大爆炸半径 |
| --- | --- | --- |
| 应用被 RCE | 数据库账号只读、只能访问自己的表 | 数据库超级用户、能读所有库 |
| 服务器被控 | 云凭据只能读一个存储桶 | 实例角色是 `AdministratorAccess` |
| 数据库被拖 | 密码是 argon2、手机号加密存储 | 明文密码、明文身份证 |
| 一个容器被逃逸 | 网络隔离，只能访问它需要的服务 | 扁平网络，能横向到所有机器 |
| 一个密钥泄露 | 短期凭据，15 分钟后失效 | 长期 AccessKey，永久有效 |

**右边那一列的每一项，都不是「被攻击」造成的，是「设计选择」造成的。** 这是好消息——它们全部可以在没有攻击发生时提前改掉。

## 最小权限的三个落点

### 一、数据库

```sql
-- ❌ 应用用超级用户连库
-- ✅ 每个服务一个账号，按需授权

CREATE USER app_web WITH PASSWORD '...';
GRANT CONNECT ON DATABASE mydb TO app_web;
GRANT USAGE ON SCHEMA public TO app_web;

-- 只给用到的表，只给用到的操作
GRANT SELECT, INSERT, UPDATE ON orders, order_items TO app_web;
GRANT SELECT ON products TO app_web;
-- 注意:没给 DELETE，也没给 DROP

-- 报表服务只读
CREATE USER app_report WITH PASSWORD '...';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_report;
```

<Callout type="info">
  **不给 `DELETE` 和 `DROP` 是很有效的一招。** 即使发生 SQL 注入，攻击者也**删不掉数据**——数据泄露和数据被毁是两个量级的事故。前者你还有客户，后者可能直接倒闭。

  需要「删除」功能时，用**软删除**（`deleted_at` 字段）配合 `UPDATE` 权限，物理删除交给单独的、权限受控的清理任务。
</Callout>

再往上一层是 [授权那章](https://daiw.net/manual/web-security/authz)讲的 **RLS（行级安全）**——它让即使有注入也拖不走别人的数据。

### 二、云权限

```json
// ❌ 图省事
{ "Effect": "Allow", "Action": "s3:*", "Resource": "*" }

// ✅ 精确到操作与资源
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-uploads/*",
  "Condition": { "StringEquals": { "s3:x-amz-server-side-encryption": "AES256" } }
}
```

**怎么知道该给哪些权限**：先给最小集合，跑起来看报错，缺什么补什么。云厂商大多提供「访问顾问」类功能，能告诉你某个角色实际用过哪些权限——**据此裁掉从未使用的**。

### 三、网络

**默认拒绝，按需放行**：

```
应用服务器 → 数据库：  只允许 5432，且只从应用所在网段
应用服务器 → 公网：    只允许 443，且走出网代理（见 SSRF 那章）
数据库     → 任何地方： 不允许主动外呼
管理端口   → 只允许跳板机
```

**「数据库不允许主动外呼」**这条经常被忽略，但它能挡住一整类数据外带——攻击者拿到数据库权限后想把数据传出去，发现连不上外网。

## 让失败倒向安全的方向

这是设计层面最重要的一条原则。**当某个组件出故障时，系统应该「关门」而不是「敞开」。**

```javascript
// ❌ 鉴权服务挂了就放行 —— 攻击者只要把鉴权服务打挂就能进
try {
  const user = await authService.verify(token);
  req.user = user;
} catch (e) {
  logger.warn('auth service down, allowing request');   // 灾难
}

// ✅ 鉴权服务挂了就拒绝
let user;
try {
  user = await authService.verify(token);
} catch (e) {
  logger.error('auth service error', e);
  return res.status(503).json({ error: 'service unavailable' });
}
```

同样的原则贯穿各处：

| 场景 | 安全的失败方向 |
| --- | --- |
| 权限规则表里查不到这条路由 | **拒绝**（[授权那章](https://daiw.net/manual/web-security/authz)的默认拒绝） |
| 限流服务不可用 | 拒绝或降级到本地限流，**不要无限放行** |
| CSP 报告端点挂了 | 不影响 CSP 本身生效 |
| 证书校验失败 | 断开连接，**不要「忽略证书错误」** |
| 输入解析出异常 | 拒绝这个输入，不要「尽力猜测」 |

<Callout type="warn">
  **最后一条对应的是 Postel 定律（「发送时严格，接收时宽容」）在安全场景下的失效。**

  「接收时宽容」在互联互通上是美德，在安全上是灾难——[请求走私](https://daiw.net/manual/web-security/request-smuggling)的根因正是两个组件对畸形输入「各自宽容地猜测」，猜出了不同结果。

  **涉及安全边界的解析，宁可拒绝也不要猜。**
</Callout>

## 分层：每一层挡住不同的东西

一个 XSS 从产生到造成损失，要穿过多少层？

```
① 输入校验        → 挡掉明显的畸形输入
② 输出编码        → 让数据不变成代码            ← 主要防线
③ CSP             → 注入的脚本执行不了
④ connect-src     → 就算执行了，数据传不出去
⑤ HttpOnly Cookie → 偷不到会话 token
⑥ 敏感操作二次验证 → 有会话也改不了密码
⑦ 异常行为检测    → 事后能发现
```

**任何一层单独都不完美，但要全部穿透就很难了。** 这就是纵深防御的算术：不是「七层里最强的那层决定安全性」，而是**攻击者要连过七关**。

反过来看，也解释了为什么某些配置特别值钱：`HttpOnly` 只是一个 Cookie 属性，但它单独就废掉了「XSS 偷 Cookie」这条最常见的利用链。

## 隔离：把不同信任级别分开

| 分开什么 | 为什么 |
| --- | --- |
| 生产 / 测试环境 | 测试环境的防护和访问控制天然更松 |
| 用户上传内容 / 主站 | **独立域名**，见[文件上传](https://daiw.net/manual/web-security/file-upload) |
| 管理后台 / 用户前台 | 后台限制来源 IP 或走内网/VPN |
| 处理不可信输入的组件 | 图片处理、PDF 渲染、HTML 解析 → 独立进程/容器，权限最小 |
| 不同租户的数据 | 至少 schema 级隔离，敏感场景物理隔离 |

**「处理不可信输入的组件单独隔离」**这条特别值得做。图片处理库、PDF 渲染器、压缩解压库——这些是历史上 CVE 最密集的地方，而且它们处理的恰恰是攻击者完全控制的字节。把它们关进一个没有网络、只读文件系统、低权限的沙箱里跑，收益极高。

## 别忘了可恢复性

安全的最后一环是**出事之后能恢复**。

```
备份：3-2-1 原则 —— 3 份副本、2 种介质、1 份异地
```

**关键点：备份必须离线或不可变。**

<Callout type="warn">
  **能被生产环境凭据删除的备份，在勒索软件面前等于不存在。**

  现代勒索攻击的标准流程就是**先删备份再加密数据**。如果你的备份和生产用同一套凭据、放在同一个云账号、挂在同一个网络里——攻击者一并处理掉。

  对策：
  - 备份存储用**独立的凭据**，生产环境的账号**只有写入权限，没有删除权限**；
  - 开启对象存储的**版本控制 + 对象锁定**（WORM，写入后一定期限内不可删改）；
  - **定期演练恢复**——没验证过的备份，只是一堆你以为能用的文件。
</Callout>

## 自检：三个假设

拿这三个问题过一遍你的系统，答不上来的地方就是待办：

1. **假设一个普通用户账号被完全控制**——他能看到、改到别人的数据吗？能碰到管理功能吗？
2. **假设应用进程被 RCE**——它手上的数据库凭据能干什么？云凭据能干什么？能连到内网哪些机器？
3. **假设整个数据库被拖走并公开**——里面哪些字段会造成不可挽回的损失？它们加密了吗？

## 小结

- 正确的问题不是「会不会被打穿」，而是**「打穿之后能波及多远」**。
- 爆炸半径的大小是**设计选择**，可以在事发前改掉。
- 最小权限落在三处：**数据库（不给 DELETE/DROP）、云权限（精确到操作与资源）、网络（默认拒绝，数据库禁止外呼）**。
- **失败要倒向安全方向**：鉴权服务挂了要拒绝，不是放行；解析异常要拒绝，不要猜。
- 纵深防御的算术是**攻击者要连过所有关**，所以 `HttpOnly` 这种「一行配置」也很值钱。
- **处理不可信输入的组件单独隔离**（图片、PDF、解压）收益极高。
- **备份必须是生产凭据删不掉的**，且要定期演练恢复。

下一章讲怎么知道自己被打了。👉 [日志、监控与检测](https://daiw.net/manual/web-security/logging-and-detection)
