纵深防御与最小权限

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

作者 David更新于 第 26 篇(共 29 篇)

纵深防御与最小权限

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

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

问错问题的样子:

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

问对问题的样子:

「假设攻击者已经拿到了一个普通用户的会话,他接下来能做什么?」 「假设应用被 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,写入后一定期限内不可删改);
  • 定期演练恢复——没验证过的备份,只是一堆你以为能用的文件。

自检:三个假设

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

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

小结

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

下一章讲怎么知道自己被打了。👉 日志、监控与检测

本页目录