纵深防御与最小权限
前面每一层都会被突破——这一章讲怎么让「被突破」不等于「全线崩溃」:爆炸半径怎么算、失败要往安全的方向倒、以及最小权限在数据库、云、网络三处的具体落法。
纵深防御与最小权限
前面十几章讲的每一道防线,都会在某个时刻被突破。这一章的主题是:那之后会发生什么。
从「会不会被打穿」到「打穿之后呢」
问错问题的样子:
「我们的登录接口安全吗?」
问对问题的样子:
「假设攻击者已经拿到了一个普通用户的会话,他接下来能做什么?」 「假设应用被 RCE 了,攻击者手上的凭据能碰到哪些系统?」 「假设数据库被拖走了,里面的数据泄露出去有多严重?」
这三个「假设」就是纵深防御的思考方式。 每一层都要在「上一层已经失守」的前提下设计。
爆炸半径
给你的每个组件算一笔账:它被完全控制之后,能波及多远?
| 场景 | 小爆炸半径 | 大爆炸半径 |
|---|---|---|
| 应用被 RCE | 数据库账号只读、只能访问自己的表 | 数据库超级用户、能读所有库 |
| 服务器被控 | 云凭据只能读一个存储桶 | 实例角色是 AdministratorAccess |
| 数据库被拖 | 密码是 argon2、手机号加密存储 | 明文密码、明文身份证 |
| 一个容器被逃逸 | 网络隔离,只能访问它需要的服务 | 扁平网络,能横向到所有机器 |
| 一个密钥泄露 | 短期凭据,15 分钟后失效 | 长期 AccessKey,永久有效 |
右边那一列的每一项,都不是「被攻击」造成的,是「设计选择」造成的。 这是好消息——它们全部可以在没有攻击发生时提前改掉。
最小权限的三个落点
一、数据库
-- ❌ 应用用超级用户连库
-- ✅ 每个服务一个账号,按需授权
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;不给 DELETE 和 DROP 是很有效的一招。 即使发生 SQL 注入,攻击者也删不掉数据——数据泄露和数据被毁是两个量级的事故。前者你还有客户,后者可能直接倒闭。
需要「删除」功能时,用软删除(deleted_at 字段)配合 UPDATE 权限,物理删除交给单独的、权限受控的清理任务。
再往上一层是 授权那章讲的 RLS(行级安全)——它让即使有注入也拖不走别人的数据。
二、云权限
// ❌ 图省事
{ "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 那章)
数据库 → 任何地方: 不允许主动外呼
管理端口 → 只允许跳板机**「数据库不允许主动外呼」**这条经常被忽略,但它能挡住一整类数据外带——攻击者拿到数据库权限后想把数据传出去,发现连不上外网。
让失败倒向安全的方向
这是设计层面最重要的一条原则。当某个组件出故障时,系统应该「关门」而不是「敞开」。
// ❌ 鉴权服务挂了就放行 —— 攻击者只要把鉴权服务打挂就能进
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' });
}同样的原则贯穿各处:
| 场景 | 安全的失败方向 |
|---|---|
| 权限规则表里查不到这条路由 | 拒绝(授权那章的默认拒绝) |
| 限流服务不可用 | 拒绝或降级到本地限流,不要无限放行 |
| CSP 报告端点挂了 | 不影响 CSP 本身生效 |
| 证书校验失败 | 断开连接,不要「忽略证书错误」 |
| 输入解析出异常 | 拒绝这个输入,不要「尽力猜测」 |
最后一条对应的是 Postel 定律(「发送时严格,接收时宽容」)在安全场景下的失效。
「接收时宽容」在互联互通上是美德,在安全上是灾难——请求走私的根因正是两个组件对畸形输入「各自宽容地猜测」,猜出了不同结果。
涉及安全边界的解析,宁可拒绝也不要猜。
分层:每一层挡住不同的东西
一个 XSS 从产生到造成损失,要穿过多少层?
① 输入校验 → 挡掉明显的畸形输入
② 输出编码 → 让数据不变成代码 ← 主要防线
③ CSP → 注入的脚本执行不了
④ connect-src → 就算执行了,数据传不出去
⑤ HttpOnly Cookie → 偷不到会话 token
⑥ 敏感操作二次验证 → 有会话也改不了密码
⑦ 异常行为检测 → 事后能发现任何一层单独都不完美,但要全部穿透就很难了。 这就是纵深防御的算术:不是「七层里最强的那层决定安全性」,而是攻击者要连过七关。
反过来看,也解释了为什么某些配置特别值钱:HttpOnly 只是一个 Cookie 属性,但它单独就废掉了「XSS 偷 Cookie」这条最常见的利用链。
隔离:把不同信任级别分开
| 分开什么 | 为什么 |
|---|---|
| 生产 / 测试环境 | 测试环境的防护和访问控制天然更松 |
| 用户上传内容 / 主站 | 独立域名,见文件上传 |
| 管理后台 / 用户前台 | 后台限制来源 IP 或走内网/VPN |
| 处理不可信输入的组件 | 图片处理、PDF 渲染、HTML 解析 → 独立进程/容器,权限最小 |
| 不同租户的数据 | 至少 schema 级隔离,敏感场景物理隔离 |
**「处理不可信输入的组件单独隔离」**这条特别值得做。图片处理库、PDF 渲染器、压缩解压库——这些是历史上 CVE 最密集的地方,而且它们处理的恰恰是攻击者完全控制的字节。把它们关进一个没有网络、只读文件系统、低权限的沙箱里跑,收益极高。
别忘了可恢复性
安全的最后一环是出事之后能恢复。
备份:3-2-1 原则 —— 3 份副本、2 种介质、1 份异地关键点:备份必须离线或不可变。
能被生产环境凭据删除的备份,在勒索软件面前等于不存在。
现代勒索攻击的标准流程就是先删备份再加密数据。如果你的备份和生产用同一套凭据、放在同一个云账号、挂在同一个网络里——攻击者一并处理掉。
对策:
- 备份存储用独立的凭据,生产环境的账号只有写入权限,没有删除权限;
- 开启对象存储的版本控制 + 对象锁定(WORM,写入后一定期限内不可删改);
- 定期演练恢复——没验证过的备份,只是一堆你以为能用的文件。
自检:三个假设
拿这三个问题过一遍你的系统,答不上来的地方就是待办:
- 假设一个普通用户账号被完全控制——他能看到、改到别人的数据吗?能碰到管理功能吗?
- 假设应用进程被 RCE——它手上的数据库凭据能干什么?云凭据能干什么?能连到内网哪些机器?
- 假设整个数据库被拖走并公开——里面哪些字段会造成不可挽回的损失?它们加密了吗?
小结
- 正确的问题不是「会不会被打穿」,而是**「打穿之后能波及多远」**。
- 爆炸半径的大小是设计选择,可以在事发前改掉。
- 最小权限落在三处:数据库(不给 DELETE/DROP)、云权限(精确到操作与资源)、网络(默认拒绝,数据库禁止外呼)。
- 失败要倒向安全方向:鉴权服务挂了要拒绝,不是放行;解析异常要拒绝,不要猜。
- 纵深防御的算术是攻击者要连过所有关,所以
HttpOnly这种「一行配置」也很值钱。 - 处理不可信输入的组件单独隔离(图片、PDF、解压)收益极高。
- 备份必须是生产凭据删不掉的,且要定期演练恢复。
下一章讲怎么知道自己被打了。👉 日志、监控与检测