Featured image of post 以所持之物而非身份决定权限:基于能力的安全(Capability-Based Security)架构

以所持之物而非身份决定权限:基于能力的安全(Capability-Based Security)架构

深入解析基于能力的安全架构:对比巴特勒·兰普森访问控制矩阵的列切分(ACL)与行切分(Capability)、诺姆·哈迪1988年困惑的代理人问题(CSRF/SSRF的根源)、对象能力模型(OCap)与最小权限原则(POLA)、通过Facet实现的权限衰减、Redell看门人模式与Membrane撤销机制,以及seL4、FreeBSD Capsicum、CHERI硬件能力指针与WebAssembly WASI的现代工程实践。

在现代操作系统、云基础设施与Web应用程序中,毁灭性的安全漏洞往往呈现出惊人相似的结构模式。跨站请求伪造(CSRF)、服务端请求伪造(SSRF)、SQL与命令注入、容器逃逸,以及 npm 或 PyPI 生态中的软件供应链攻击,表面上处于软件栈的不同层级,但其底层却共享着同一个架构缺陷。

这一缺陷源于计算机工业半个多世纪以来对访问控制列表(Access Control Lists, ACL)与环境权限(Ambient Authority)的过度依赖——即根据“你是谁(主体身份 Subject Identity)”而非“你持有什么(所持凭证 What You Hold)”来裁决访问权限,并将资源指定(Designation,如可伪造的字符串路径或URL)与访问授权(Authority,调用方携带的环境身份)彻底割裂。

本文将系统剖析能够从根源上消除这一结构性漏洞的替代范式:基于能力的安全(Capability-Based Security)。我们将从巴特勒·兰普森(Butler Lampson)1971年提出的访问控制矩阵 $M[S, O]$ 的数学分解出发,还原诺姆·哈迪(Norm Hardy)1988年经典的“困惑的代理人问题(Confused Deputy Problem)”,阐述马克·米勒(Mark S. Miller)的对象能力模型(OCap)与最小权限原则(POLA),展示权限衰减(Facet)与即时/传递性撤销(Redell Caretaker 与 Membrane)的代码模式,并考察形式化验证微内核 seL4、FreeBSD Capsicum、CHERI / ARM Morello 硬件能力指针以及 WebAssembly WASI 的现代工程实现。


1. 两种权限范式:身份胸牌 vs. 泊车钥匙(Valet Key)

在进入操作系统形式化理论之前,不妨对比现实物理世界中的两种安防机制。

范式 A:身份胸牌与门禁名册(ACL 模型)

假设在一座高安全级别的办公大楼里,每扇门前都有一名保安手持登记册(即访问控制列表 ACL)。当你要求进入 302 室时,保安检查你的员工胸牌(身份 Identity / uid),核对你的名字是否在 302 室的准入名单上。

现在,你想让一名实习生(辅助程序或第三方库)去 302 室的文件柜里取一份 Q3_Budget.pdf。由于实习生不在名单上,你只能把自己的员工胸牌借给实习生(如同 UNIX 的 setuid 或以当前用户身份运行子程序)。一旦实习生戴上你的胸牌进入 302 室,他就自动获得了对该房间内所有机密文件的环境权限(Ambient Authority)。更糟的是,如果外部攻击者篡改了便签,诱骗实习生去取走 Payroll_Master.db(主薪资数据库),门口的保安依然会放行,因为实习生佩戴着你的合法胸牌!

范式 B:汽车代客泊车钥匙(Capability 模型)

对比之下,当你把车交给酒店泊车员时,你不会去车管所修改车辆的访问控制名单,也不会把自己的身份证交给泊车员,而是递给他一把泊车钥匙(Valet Key)。这把钥匙体现了三个核心架构特性:

  1. 指定与授权的统一(Unification of Designation and Authority):钥匙既唯一指定了要操作哪辆车,又同时授予了启动引擎的权限。车辆不关心持有人是谁,持有不可伪造的钥匙本身就是权限的充要证明。
  2. 权限衰减(Attenuation / 最小权限):泊车钥匙无法打开后备箱或手套箱,甚至限制了最高车速。它仅授予完成泊车所需的最小权限。
  3. 去中心化委托(Decentralized Delegation):泊车员交接班时,只需直接传递这把钥匙,无需修改任何中央权限数据库。

在计算机科学中,范式 A 即 ACL 模型,而范式 B 则是基于能力的安全(Capability)模型。


2. 兰普森访问控制矩阵 $M[S, O]$ 的两种正交分解

1971年,图灵奖得主巴特勒·兰普森发表了《Protection》,提出了访问控制矩阵(Access Control Matrix)作为操作系统保护机制的通用数学抽象(后经 Harrison、Ruzzo 与 Ullman 形式化为 HRU 模型)。系统的保护状态由三元组 $(S, O, M)$ 定义:

  • 主体集合 $S = \{s_1, s_2, \dots, s_m\}$:发起操作的主动实体(用户、进程、线程、软件模块)。
  • 客体集合 $O = \{o_1, o_2, \dots, o_n\}$:受保护的资源(文件、内存段、IPC 端点、套接字、硬件设备)。通常 $S \subseteq O$。
  • 权限全集 $\mathcal{R} = \{\mathsf{read}, \mathsf{write}, \mathsf{execute}, \mathsf{append}, \mathsf{grant}, \mathsf{own}\}$:允许在客体上执行的离散操作。
  • 访问控制矩阵 $M : S \times O \to 2^{\mathcal{R}}$:矩阵元素 $M[s, o] \subseteq \mathcal{R}$ 定义了主体 $s \in S$ 对客体 $o \in O$ 拥有的确切权限子集。

例如,包含三个主体 $S = \{\text{Alice}, \text{Compiler}, \text{Logger}\}$ 与三个文件客体 $O = \{\texttt{src.f}, \texttt{/SYSX/STAT}, \texttt{/SYSX/BILL}\}$ 的矩阵如下:

$$ M[S, O] = \begin{array}{c|ccc} & \texttt{src.f} & \texttt{/SYSX/STAT} & \texttt{/SYSX/BILL} \\ \hline \text{Alice} & \{\mathsf{read}, \mathsf{write}\} & \emptyset & \emptyset \\ \text{Compiler} & \{\mathsf{read}\} & \{\mathsf{append}\} & \{\mathsf{read}, \mathsf{write}\} \\ \text{Logger} & \emptyset & \{\mathsf{read}, \mathsf{write}\} & \emptyset \end{array} $$

在实际系统中,$|S|$ 与 $|O|$ 动辄数百万且矩阵极度稀疏,直接存储二维数组需要 $\mathcal{O}(|S| \times |O|)$ 空间。因此,架构师必须沿着两个正交维度之一对稀疏矩阵 $M[S, O]$ 进行切分:按列切分(依附于客体的列表),或按行切分(由主体持有的令牌)。

  flowchart TD
    ACM["巴特勒·兰普森访问控制矩阵 M[S, O]<br/>行: 主体 S / 列: 客体 O"]
    ACM -->|"按列切分 (Per-Object)"| ACL["访问控制列表 (ACL)<br/>每个客体 o 存储二元组 (主体 s, 权限 R)"]
    ACM -->|"按行切分 (Per-Subject)"| CAP["能力模型 (Capabilities / C-List / OCap)<br/>每个主体 s 持有不可伪造的句柄 Cap(o, R)"]

    ACL --> ACL_PROP["1. 将资源指定 ('/etc/passwd') 与授权 (环境 uid) 分离<br/>2. 进程在每次调用中同时携带其全部权限 (Ambient Authority)<br/>3. 在结构上易受困惑的代理人攻击 (CSRF / SSRF)"]
    CAP --> CAP_PROP["1. 将资源指定 (哪个客体 o) 与授权 (权限 R) 绑定为单一句柄<br/>2. 无权限即无命名空间可见性 (No Designation Without Authority)<br/>3. 原生支持极细粒度的最小权限原则 (POLA) 与安全委托"]

2.1 按列切分:访问控制列表(ACL)

沿着矩阵 $M[S, O]$ 的列(Column)垂直切分,即为每个客体 $o \in O$ 关联一份非空权限条目列表:

$$ \text{ACL}(o) = \bigl\{ (s, M[s, o]) \;\big|\; s \in S,\; M[s, o] \neq \emptyset \bigr\} $$

UNIX/POSIX 权限、Windows NTFS DACL 与云端 IAM 资源策略均采用按列切分。这种设计导致了指定(Designation)与授权(Authority)的分离:程序通过全局公开的字符串路径(如 "/SYSX/BILL")指定目标,而内核引用监控器(Reference Monitor)则检查调用方进程的环境身份(uid、euid、会话 Cookie)是否在目标的 ACL 中。这意味着进程在每一次系统调用中都默认挥舞着其整行权限的并集 $\bigcup_{o \in O} M[s, o]$,即环境权限(Ambient Authority)。

2.2 按行切分:能力(Capabilities,Dennis & Van Horn 1966)

1966年,Jack B. Dennis 与 Earl C. Van Horn 在论文《Programming Semantics for Multiprogrammed Computations》中提出了对偶方案:沿着矩阵 $M[S, O]$ 的行(Row)水平切分,为每个主体 $s \in S$ 分配一个能力空间(CSpace 或 C-List):

$$ \text{CSpace}(s) = \bigl\{ \text{Cap}(o, R) \;\big|\; o \in O,\; R = M[s, o] \neq \emptyset \bigr\} $$

其中每个元素 $c = \text{Cap}(o, R)$ 称为一个能力(Capability)——它是一个不可伪造的权限令牌,将指定(指向特定客体 $o$ 的指针或句柄)与授权(允许的操作子集 $R \subseteq \mathcal{R}$)不可分割地绑定在一起。在纯能力系统中,没有全局文件路径命名空间,访问时也不检查调用者的用户身份。若主体 $s$ 未持有客体 $o$ 的能力,它甚至无法在命名空间中“指名”客体 $o$。这确立了能力安全的核心公理:无授权即无指定,无指定即无授权(No Designation Without Authority)。

架构维度访问控制列表(ACL —— 按列切分)基于能力的安全(Capabilities / OCap —— 按行切分)
矩阵 $M[S, O]$ 的分解方式按列存储在每个受保护客体 $o \in O$ 上按行存储在每个主体 $s \in S$ 的能力空间中
核心授权哲学你是谁(Who You Are):检查调用方身份 uid你持有什么(What You Hold):检查出示的能力令牌
资源指定与授权的关系彻底分离(可伪造的字符串路径 + 环境身份)完全统一(不可伪造的句柄同时指定 $o$ 并授予 $R$)
权限激活模型环境权限(Ambient Authority):所有权限时刻激活选择性权限(Selective Authority):仅显式传递的句柄生效
困惑的代理人脆弱性结构性脆弱(易触发 CSRF、SSRF、setuid 提权)结构性免疫(调用方必须亲自传递已授权的客体句柄)
代表性系统实现UNIX/POSIX ACL, Windows NTFS DACL, AWS IAMKeyKOS, seL4, FreeBSD Capsicum, Fuchsia Zircon, CHERI, WASI

3. 困惑的代理人问题(Confused Deputy):Norm Hardy 的 1988 经典案例与现代变体

为什么分离“指定”与“授权”会带来灾难?计算机架构师诺姆·哈迪(Norm Hardy)在1988年10月发表于 ACM SIGOPS Operating Systems Review 的经典论文《The Confused Deputy (or why capabilities might have been invented)》中,记录了20世纪70年代中期发生在 Tymshare 商业操作系统上的一起真实安全事故。

3.1 /SYSX/FORT 编译器覆写计费数据库事故

在 Tymshare 系统中,Fortran 编译器位于 /SYSX/FORT:

  1. 统计收集需求:系统管理员希望每次用户编译程序时,编译器都能向 /SYSX/STAT 文件追加一条统计记录。
  2. 授予环境权限:为此,管理员为 /SYSX/FORT 可执行文件授予了对整个 /SYSX 目录的环境写入权限(类似于 UNIX 的 setuid)。
  3. 用户自定义输出路径:编译器允许用户传入一个字符串文件名参数,用于保存调试列表输出(例如 FORT(SRC="prog.f", OUT="listing.txt"))。
  4. 核心资产 /SYSX/BILL:同一个 /SYSX 目录下还存放着记录所有客户欠款的主计费数据库 /SYSX/BILL,普通用户对其没有任何写权限。

某天,一名用户调用 /SYSX/FORT,并将输出文件名指定为字符串 "/SYSX/BILL"。编译器随后执行了 open("/SYSX/BILL", WRITE)。操作系统的 ACL 引用监控器拦截该调用并询问:“当前进程(/SYSX/FORT)是否有权写入 /SYSX 目录?” 答案是肯定的!于是内核放行,编译器忠实地用 Fortran 调试输出覆盖并摧毁了公司的核心计费数据库 /SYSX/BILL!

  flowchart TD
    subgraph ACL_WORLD["ACL + 环境权限模型 (困惑的代理人漏洞)"]
        U1["恶意用户<br/>(无权访问 /SYSX/BILL)"] -->|"1. 传入字符串路径 OUT='/SYSX/BILL'"| D1["代理人: 编译器 /SYSX/FORT<br/>(环境权限: 可写入 /SYSX/*)"]
        D1 -->|"2. open('/SYSX/BILL', WRITE)<br/>隐式动用编译器自身的环境权限"| OS1["操作系统 ACL 检查器"]
        OS1 -->|"3. 编译器拥有 /SYSX 写权限 -> 放行!"| BILL1["主计费数据库 /SYSX/BILL<br/>[被覆写损毁!]"]
    end

    subgraph CAP_WORLD["能力模型 (指定与授权统一)"]
        U2["普通用户<br/>(仅持有自身文件的能力)"] -->|"1. 传入输出文件能力句柄 cap_out<br/>(无法传入 /SYSX/BILL 的能力, 因为用户并未持有!)"| D2["代理人: 编译器 /SYSX/FORT<br/>(安装时仅被赋予私有能力 cap_stat)"]
        D2 -->|"2a. write(cap_out, debug_output)<br/>仅使用用户显式委托的权限"| OUT2["用户的输出文件 listing.txt"]
        D2 -->|"2b. append(cap_stat, usage_stats)<br/>使用编译器私有的统计文件能力"| STAT2["统计文件 /SYSX/STAT"]
    end

3.2 为什么编译器会“困惑”?以及现代漏洞的同构性

哈迪将 /SYSX/FORT 称为困惑的代理人(Confused Deputy),因为它同时充当了两个委托人(系统管理员与调用用户)的代理人,但由于操作系统分离了指定(字符串路径)与授权(环境权限),编译器在打开用户指定的路径时,操作系统自动动用了管理员赋予编译器的环境权限。

在能力架构下,这一漏洞从根本上无法被表达:编译器在安装时仅被赋予单个文件句柄 $c_{\text{stat}} = \text{Cap}(\texttt{/SYSX/STAT}, \{\mathsf{append}\})$;用户调用编译器时,必须自己在命名空间中打开输出文件,并将写能力句柄 $c_{\text{out}} = \text{Cap}(f_{\text{out}}, \{\mathsf{write}\})$ 传给编译器。用户没有 /SYSX/BILL 的能力句柄,自然无法将其传递给编译器。

现代网络安全中最具破坏力的漏洞类别,本质上都是困惑的代理人:

  • 跨站请求伪造(CSRF):浏览器是困惑的代理人。当恶意网站 evil.example 向 https://bank.example/transfer 发起请求时,浏览器看到目标域名字符串,便自动附加上用户的环境权限(bank.example 的会话 Cookie)。CSRF Token 之所以有效,正是因为它要求请求显式携带不可伪造的能力令牌。
  • 服务端请求伪造(SSRF):Web 服务器作为代理人接收用户传入的 URL 字符串(如 "http://169.254.169.254/latest/meta-data/"),并动用服务器自身在云内网中的环境网络权限去读取 IAM 密钥。
  • 软件供应链攻击(npm / PyPI 恶意依赖):一个仅 20 行代码的字符串格式化第三方库,之所以能读取 ~/.ssh/id_rsa 并外发,是因为在 POSIX ACL 模型下,进程内的每一个函数都无条件继承了当前用户 uid 的全部环境权限。

4. 对象能力模型(OCap)与权限衰减、即时撤销模式

1975年,Saltzer 与 Schroeder 提出了最小权限原则(Principle of Least Privilege,在能力社区中更精确地称为最小授权原则 POLA,Principle of Least Authority)。2006年,马克·米勒(Mark S. Miller)在其约翰斯·霍普金斯大学博士论文《Robust Composition》中证明:内存安全的面向对象语言天然具备在单个对象与方法调用粒度上实施 POLA 的机制——对象引用(Object Reference)本身就是一种能力。

4.1 满足 OCap 的三大语言不变量与引用传播三定律

要使内存安全语言(如 Hardened JavaScript / SES、E 语言、Wasm 组件或受限 Rust)成为严格的对象能力(OCap)系统,运行时必须满足三条不变量:

  1. 不可伪造引用(No Forged References):禁止将整数强转为对象指针或越界内存访问。
  2. 无可变全局状态与零环境权限(No Mutable Global State):全局作用域不得包含可变的共享属性或直接调用的 I/O 模块(如 fs、net),内置原型链必须深度冻结(Object.freeze)。
  3. 绝对封装(Absolute Encapsulation):对象的私有词法闭包状态或私有字段无法从外部通过反射窃取。

在 OCap 引用图 $G = (V, E)$ 中,主体 $A$ 获得客体 $C$ 的引用 $(A \to C)$ 只有三种途径:1. 创造(Parenthood,new C());2. 禀赋(Endowment,创建闭包时注入);3. 介绍(Introduction,同时持有 $B \to A$ 与 $B \to C$ 的介绍人 $B$ 通过消息传递将 $C$ 交给 $A$):

$$ \text{介绍定律 (Law of Introduction)}: \quad (B \to A) \;\land\; (B \to C) \;\;\Longrightarrow\;\; B \text{ 可建立 } (A \to C) $$

4.2 权限衰减(Facet)与 Redell 看门人撤销模式(Caretaker & Membrane)

传统教科书常有一个误区:“能力一旦交给不可信方,就无法限制其用途,也无法撤销。” 事实上,David D. Redell 早在1974年的伯克利博士论文中就解决了能力撤销问题。利用词法闭包与代理,我们可以用十余行代码同时实现细粒度衰减(Facet)与即时撤销(Caretaker):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 权限衰减切面 (Attenuating Facet): 仅暴露特定前缀的只读能力
function makeScopedReadFacet(fullStorageCap, allowedPrefix) {
  return Object.freeze({
    read(key) {
      if (typeof key !== "string" || !key.startsWith(allowedPrefix)) {
        throw new Error(`拒绝访问: 键名必须以 '${allowedPrefix}' 开头`);
      }
      return fullStorageCap.read(key);
    }
    // write() 与 deleteAll() 被彻底隐藏在闭包之外!
  });
}

// 2. Redell (1974) 看门人模式 (Caretaker Pattern): 实现 O(1) 即时权限撤销
function makeCaretaker(targetCap) {
  let currentTarget = targetCap; // 闭包内的私有可变引用
  const caretaker = Object.freeze({
    read(...args) {
      if (currentTarget === null) throw new Error("能力已被撤销 (Revoked)");
      return currentTarget.read(...args);
    },
    write(...args) {
      if (currentTarget === null) throw new Error("能力已被撤销 (Revoked)");
      return currentTarget.write(...args);
    }
  });
  const revoker = Object.freeze({
    revoke() { currentTarget = null; } // 切断引用链并允许垃圾回收
  });
  return Object.freeze({ caretaker, revoker });
}

若目标能力是一个会返回子对象引用的复杂对象图(例如目录句柄通过 getSubDir() 返回子目录句柄),马克·米勒提出的可撤销膜模式(Revocable Membrane)利用 Proxy 与 WeakMap 递归地将所有跨过膜边界的返回值与回调参数自动包裹在同一层撤销门控中。一旦调用 revoke(),整个子图的所有衍生引用在瞬间全部失效。


5. 现代工业级部署:seL4、FreeBSD Capsicum、CHERI 与 WebAssembly WASI

首先必须澄清一个常见的术语混淆:Linux 内核中的 capabilities(7)(如 CAP_NET_ADMIN、CAP_DAC_OVERRIDE)并不是对象能力,它们只是将 root 的环境权限拆分为约 41 个线程级标志位,不指定任何具体客体,依然完全暴露于困惑的代理人漏洞之下。真正的工业级能力架构体现在以下四大系统中:

  1. seL4 形式化验证微内核:全球首个拥有完整 Isabelle/HOL 机器证明的操作系统微内核。seL4 内核实行零环境权限,所有内核资源(线程控制块 TCB、虚拟地址空间 VSpace、IPC 端点、物理内存页帧)均只能通过线程 CSpace(CNode 表)中的能力句柄访问。通过 seL4_CNode_Mint 可对能力进行权限位掩码衰减或附加不可伪造的身份徽章(Badge),并通过内核维护的能力派生树(CDT)执行传递性撤销 seL4_CNode_Revoke。
  2. FreeBSD Capsicum 沙箱框架:由剑桥大学与 Google 研发,将标准 UNIX 文件描述符(fd)提升为真正的能力。进程调用 cap_enter() 进入能力模式后,内核将无条件拦截所有访问全局命名空间的系统调用(如 open()、connect()、bind(),返回 ECAPMODE 错误)。进程只能操作已持有的文件描述符,并通过 openat(dir_fd, "sub/file", ...) 在受限子目录内进行相对查找(严禁 ".." 逃逸),同时利用 cap_rights_limit(fd, &rights) 细粒度剥离超过 60 种描述符权限位。
  3. CHERI 与 ARM Morello 硅片级硬件能力指针:由剑桥大学与 SRI 联合设计、ARM 在 Morello 处理器(7nm 四核架构)及 CHERIoT-RISC-V 中流片实现的硬件架构。CHERI 将传统的 64 位整数指针替换为 128 位硬件能力寄存器 + 1 位带外有效性标签(Tag Bit):
$$ \underbrace{\text{Tag (1 bit)}}_{\text{硬件防伪标签位}} \;\Big|\; \underbrace{\text{Permissions (18 bits)}}_{\text{读写执行权限掩码}} \;\Big|\; \underbrace{\text{Object Type (18 bits)}}_{\text{硬件封装类型}} \;\Big|\; \underbrace{\text{Compressed Bounds (28 bits)}}_{\text{浮点压缩基址与上界 [Base, Top)}} \;\Big|\; \underbrace{\text{Address Cursor (64 bits)}}_{\text{64位虚拟地址游标}} $$

每次内存加载、存储或取指时,CPU 在单周期内并行校验地址游标是否严格位于 [Base, Top) 区间内且符合权限掩码;若程序试图用普通整数指令篡改内存中的能力字节,内存控制器硬件会自动将该地址的 1 位带外 Tag 清零,使伪造的指针彻底无法解引用,从物理硅片层面根除了缓冲区溢出与 ROP 攻击。 4. WebAssembly WASI 与云端 Macaroons:WebAssembly 模块默认处于零环境权限沙箱中,WASI 运行时要求宿主显式注入预打开的目录能力描述符(如 --dir /var/app/data),模块无法打开任何未授权路径。而在分布式云微服务中,Google 提出的 Macaroons 利用嵌套 HMAC 链式签名实现了无需中央数据库协助的客户端自主权限衰减(附加时间、IP、只读等上下文限制条件 Caveats),成为现代零信任云架构的核心基石。

comments powered by Disqus