返回博客

Cloudflare 容器漏洞给我们的多租户游戏后端安全启示

发布于 2026年9月25日
Cloudflare 容器漏洞给我们的多租户游戏后端安全启示 借助 AI 生成

概要

了解 Cloudflare 容器跨租户数据泄露漏洞的完整过程,涵盖攻击原理与 PoC 验证,掌握 dm-thin 配置审计、I/O 异常检测和完整修复手册,并落实纵深防御、磁盘清理等架构原则,通过可直接应用的实战检测脚本和修复步骤保护你的多租户游戏后端数据安全,防止玩家数据泄露。

你的容器化游戏后端会为每场比赛、每个大厅、每批分析数据启动一个全新的 VM。当容器关闭时,数据就消失了——对吧?

不一定。2026 年 9 月,来自 Accomplish 的安全研究员 Oren Yomtov 发现 Cloudflare Containers 存在跨租户数据泄露漏洞。已销毁容器残留的磁盘块可被同一主机上无关的工作负载读取。恢复的数据包括目录结构、数据库页以及结构完整的 SQLite 数据库。

如果你在任何多租户容器平台上运行游戏后端,就需要理解这个漏洞的根因——dm-thin 子系统中一个名为 skip_block_zeroing 的 Linux 存储配置。本文将剖析问题出在哪里,提供一份你现在就能应用的检测与修复 runbook,并介绍能够防止此类漏洞触及玩家数据的架构原则。

精简配置(Thin Provisioning)如何导致跨租户数据泄露

Cloudflare Containers 在多租户硬件上的 Firecracker 微虚拟机中运行工作负载。每个容器都有一个由 Linux device-mapper 精简配置(dm-thin)支持的可写根磁盘。Firecracker 将该磁盘作为 /dev/vdc 暴露给客户机 VM。

精简配置的工作原理是延迟物理存储分配。当你创建一个 10 GiB 的虚拟磁盘时,它几乎不占用物理空间。当客户机写入之前未映射的区域时,块会按需从共享池中分配。Cloudflare 受影响的池使用 64 KiB 的精简块大小。

漏洞就在这里。当容器被销毁时,其精简卷映射会被删除,物理块会返回共享池以供复用。受影响的池配置了:

skip_block_zeroing

设置此标志后,dm-thin 在将新分配的块提供给下一个租户之前会跳过清零操作。完整的 64 KiB 写入会覆盖整个回收块。但 部分写入——例如 4 KiB——只会替换该区域。剩余的 60 KiB 可能保留该块先前所属者的数据。

这不是理论上的边缘情况。研究人员从残留块中恢复了 目录结构、数据库页和完整的 SQLite 数据库,涉及 24 个生产环境中的 18 个,以及横跨四大洲的 22 个底层节点中的 20 个。

为什么部分写入是关键攻击向量

读取新精简磁盘上未映射的区域会返回零——dm-thin 会在不分配物理块的情况下返回零。这是安全的。该漏洞之所以有效,是因为一次小写入会触发分配一个已回收的 64 KiB 块,而不会先将其清零。

攻击模式:

  1. 在 Workers Paid 账户上创建一个容器。
  2. 打开 /dev/vdc(可写根磁盘)。
  3. 识别与 ext4 空闲空间对应的 64 KiB 对齐区域。
  4. 向每个目标区域写入一个对齐的 4 KiB 块。
  5. 读回完整的 64 KiB 块。
  6. 只检查攻击者未覆盖的 60 KiB。

第 4 步是关键。4 KiB 写入会导致 dm-thin 从共享池中分配一个物理块。由于清零被禁用,剩余的 60 KiB 可能包含之前拥有该块的人留下的残留数据。

研究人员实际验证了什么

研究人员使用 ext4 目录块校验和(metadata_csum 功能)来区分自己的测试文件系统块与外部块。在六个生产环境中:

  • 检查了 5,614 个可测试目录块
  • 0 个块归属于研究人员自己的文件系统
  • 通过校验和分析识别出 2,700 个不同的外部目录 inode

恢复的文件类型并非垃圾数据。它们包括结构上有意义的 SQLite 数据库——在游戏后端场景中,这类数据可能包含玩家存档、会话令牌或库存记录。

检测手册:发现基础设施中的残留数据窃取

如果你在多租户基础设施上运行容器化游戏后端,需要两层检测:配置审计和运行时 I/O 异常分析。

第 1 步:审计你的 dm-thin 配置

在每个运行容器的宿主机上运行以下脚本:

#!/bin/bash
set -euo pipefail

echo "=== dm-thin Pool Configuration Audit ==="
echo ""

# Find all thin pool devices on this host
for pool in $(dmsetup status --target thin-pool 2>/dev/null | awk '{print $1}'); do
    echo "Pool: $pool"
    TABLE=$(dmsetup table "$pool")
    echo "  Full table: $TABLE"
    
    if echo "$TABLE" | grep -q "skip_block_zeroing"; then
        echo "  ⚠️  WARNING: skip_block_zeroing is ENABLED"
        echo "  Recycled blocks may retain previous tenant data."
        echo "  Action: Remove skip_block_zeroing from pool table."
    else
        echo "  ✅ Block zeroing is active (default dm-thin behavior)"
    fi
    echo ""
done

echo "=== Thin-block size check ==="
for pool in $(dmsetup status --target thin-pool 2>/dev/null | awk '{print $1}'); do
    BLOCK_SECTORS=$(dmsetup table "$pool" | grep -oP '\d+ \d+ thin-pool' | awk '{print $1}')
    BLOCK_KB=$((BLOCK_SECTORS * 512 / 1024))
    echo "  Pool $pool: block size = ${BLOCK_KB} KiB"
    if [ "$BLOCK_KB" -le 64 ]; then
        echo "    ⚠️  Small block size amplifies residual data exposure."
        echo "    Larger block sizes reduce the ratio of leftover bytes per partial write."
    fi
done

如果你的池配置中任何位置出现 skip_block_zeroing,你就存在与 Cloudflare 已修复的同类漏洞。立即修复——不要等到计划维护窗口。

第 2 步:监控特征性 I/O 签名

该漏洞会产生可检测的签名:小写入后跟着不成比例的大读取。4 KiB 写入会分配一个 64 KiB 块;随后的读取会恢复完整的 64 KiB。如果容器遥测中读取量超过写入量 10 倍以上,就可疑。

#!/usr/bin/env python3
"""
Detect anomalous write-then-read patterns characteristic of
cross-tenant residual data harvesting in thin-provisioned containers.

Usage: python detect_residual_harvesting.py io_events.jsonl

Input: JSONL file with one event per line:
  {"container_id": "abc123", "operation": "write", "size_bytes": 4096}
"""

import json
import sys
from collections import defaultdict

ANOMALY_THRESHOLD = 10.0  # read_bytes : write_bytes ratio

def analyze_io_events(events_file):
    with open(events_file) as f:
        events = [json.loads(line) for line in f]

    containers = defaultdict(
        lambda: {"writes": 0, "reads": 0, "write_bytes": 0, "read_bytes": 0}
    )

    for event in events:
        cid = event.get("container_id", "unknown")
        op = event.get("operation")
        size = event.get("size_bytes", 0)

        if op == "write":
            containers[cid]["writes"] += 1
            containers[cid]["write_bytes"] += size
        elif op == "read":
            containers[cid]["reads"] += 1
            containers[cid]["read_bytes"] += size

    print("=" * 60)
    print("CONTAINERS WITH ANOMALOUS READ/WRITE RATIOS")
    print("=" * 60)

    flagged = 0
    for cid, stats in sorted(containers.items()):
        if stats["write_bytes"] == 0:
            continue
        ratio = stats["read_bytes"] / stats["write_bytes"]
        if ratio > ANOMALY_THRESHOLD:
            flagged += 1
            print(f"\n🚩 Container: {cid}")
            print(f"   Writes: {stats['writes']:,} ops | "
                  f"{stats['write_bytes']:,} bytes")
            print(f"   Reads:  {stats['reads']:,} ops | "
                  f"{stats['read_bytes']:,} bytes")
            print(f"   Ratio:  {ratio:.1f}x — investigate")

    if flagged == 0:
        print("\n✅ No anomalous containers detected.")
    print(f"\nScanned {len(containers)} containers. Flagged: {flagged}")
    return flagged

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python detect_residual_harvesting.py <io_events.jsonl>")
        sys.exit(1)
    sys.exit(1 if analyze_io_events(sys.argv[1]) > 0 else 0)

在 Cloudflare 事件中,研究人员的概念验证(PoC)恰好产生了这种签名。Cloudflare 安全团队根据 PoC 构建了检测签名,将其应用于历史磁盘 I/O 遥测数据,并确认唯一匹配的活动来自研究人员和 Cloudflare 自己的工程师在授权验证期间的操作。未发现第三方利用。

教训是:如果你在收集容器 I/O 遥测数据,就可以进行追溯审计。如果你没有收集,那你就是在盲飞。

修复操作手册

如果你的审计发现 skip_block_zeroing 或类似的暴露,以下是 Cloudflare 执行的修复顺序——顺序很重要。

第 1 步:在所有池上重新启用块清零

从你的 dm-thin 池配置中移除 skip_block_zeroing。这是对 thin-pool 设备的运行时更改,但它只保护新的分配。已经映射到运行中容器和缓存镜像快照中的块仍然存在风险。

# This is a simplified representation — actual dm-thin 
# pool recreation commands vary by your orchestration layer.
# The core principle: restart the pool WITHOUT skip_block_zeroing.

dmsetup reload <pool-name> --table "0 <size> thin-pool <meta_dev> <data_dev> <data_block_size> 0"
dmsetup suspend <pool-name>
dmsetup resume <pool-name>

Cloudflare 在收到初始报告后约 6 小时内合并了此修复。研究人员独立确认,在此更改后 PoC 不再有效。

第 2 步:淘汰所有运行中的容器磁盘

已经映射到活动精简设备中的块会保留其(未清零的)内容。消除残留数据的唯一方法是销毁并重新创建每个容器磁盘。

Cloudflare 在非高峰时段排空宿主机,并在每台宿主机上重启 VM。这是一个滚动操作——对于游戏后端,请安排在流量最低的时段。每台宿主机会有短暂中断。如果你的 matchmaking 层支持优雅交接,排空宿主机上的玩家会被迁移而不是断开连接。

第 3 步:清除缓存的镜像快照

这一步很容易被忽略。容器编排层通常会将 OCI 镜像层缓存为 dm-thin 快照,以加快容器启动速度。这些缓存快照是在修复之前创建的,可能包含未使用区域(包括 ext4 空闲空间)中的残留数据。继承缓存层的新容器可以在不分配新块的情况下从 /dev/vdc 读取残留字节。

Cloudflare 清除了受影响集群中所有修复前创建的缓存快照,在初始报告 15 天后完成清理。缓存层使用清零后的分配重新创建。

重新创建的顺序很重要。 如果你在启用清零之前清除缓存,就等于把未清零的块直接推回新的快照中。

第 4 步:验证修复是否有效

修复后,针对新的 I/O 遥测数据运行检测脚本至少 48 小时。将写入/读取比率与补丁前的基线进行比较。特征性的高比率模式应该完全消失。

你还应在每台宿主机重启后验证 dm-thin 表:

# Confirm no host still has skip_block_zeroing
dmsetup table 2>/dev/null | grep -q "skip_block_zeroing" && \
    echo "CRITICAL: Host $(hostname) still has skip_block_zeroing" || \
    echo "OK: $(hostname) — block zeroing active"

防止此类漏洞的架构原则

Cloudflare 事件是更广泛的多租户存储隔离失败类问题中的一个实例。以下原则可以保护你的游戏后端——无论你是自己管理容器还是依赖平台。

原则 1:绝不在多租户池中禁用块清零

skip_block_zeroing 标志的存在是为了性能——每次分配时清零 64 KiB 块会增加 I/O 开销。在只运行你自己代码的单租户硬件上,这种权衡或许可以接受。在容器跨租户回收的共享基础设施上,这就是安全缺陷。

规则: 任何向多个租户身份分配块的 dm-thin 池都必须启用块清零。在基础设施即代码层强制执行这一点,确保任何宿主机都不能在没有它的情况下被配置。

原则 2:将容器磁盘视为临时且危险的

你的游戏后端绝不应假设容器磁盘在启动时是干净的。即使启用了块清零,缓存层、快照继承和内核 bug 也可能带来边缘情况。

在容器 entrypoint 中写入启动时格式化或擦除可写卷的逻辑:

#!/bin/bash
# Ensure a clean writable disk on container start
if [ -b /dev/vdc ]; then
    # Overwrite with zeros (slow but thorough)
    # For a game backend, the writable disk is typically small (1-4 GiB)
    dd if=/dev/zero of=/dev/vdc bs=1M count=4096 status=progress
    
    # Create a fresh filesystem
    mkfs.ext4 -F /dev/vdc
    
    mount /dev/vdc /workspace
fi

这会给容器启动增加几秒钟。对于一场持续 5–45 分钟的游戏比赛来说,这是可以接受的。对于亚秒级冷启动需求,使用更快的方法:blkdiscard(触发 TRIM,根据存储后端的不同可能会清零块)结合 mkfs.ext4 -F。

原则 3:将 I/O 比率作为安全信号进行监控

大多数容器监控关注 CPU、内存和网络。将磁盘 I/O 添加到你的安全遥测中。读取字节与写入字节的比率是检测残留数据窃取攻击的强信号——合法的游戏工作负载读写模式是均衡的。一个容器写入 4 KiB 块然后每个区域读回 60+ KiB,这是异常的。

如果你的后端已经记录容器生命周期事件,你可以将 I/O 异常与租户身份和创建时间戳关联起来,以缩小潜在事件的爆炸半径。对于希望获得内置崩溃报告和用户日志、又不想自己管理这套遥测基础设施的游戏开发者,像 horizOn 这样的平台开箱即用地提供了这些能力——这意味着你可以花更少时间搭建管道,更多时间投入游戏逻辑。

原则 4:为玩家数据实现纵深防御

即使你的容器磁盘泄露,如果磁盘上的数据经过加密或没有密钥就毫无意义,损害也是有限的。将玩家存档、会话令牌和库存数据加密存储。加密密钥应保存在 secrets manager 中,而不是容器磁盘上。

这与我们在 Star Citizen 数据泄露分析以及如何架构游戏后端以抵御入侵 中讨论的原则相同——纵深防御意味着假设任何单一层最终都会失败。

最佳实践清单

  1. 在部署前审计每个 dm-thin 池配置。 在容器编排管道中添加一个预检检查,如果存在 skip_block_zeroing 则失败。这是一个五行 CI 检查,可以防止一整类漏洞。

  2. 收集带租户归属的容器磁盘 I/O 遥测数据。 你无法检测到未观察到的利用行为。记录每个容器的写入/读取量,并与租户 ID 和宿主机 ID 关联。至少保留 30 天,以便进行追溯调查。

  3. 在启动时清零或擦除容器可写卷。 不要只依赖存储层。启动时执行一次全新的 mkfs.ext4 会增加 2–5 GiB 的写入开销,但能保证没有残留数据在容器回收后存活。

  4. 在安全补丁期间排空并回收容器,而不仅仅是在补丁之后。 启用块清零只保护新的分配。运行中容器和缓存镜像快照中的现有映射必须显式清除。始终在修复后执行全集群回收。

  5. 使用外部密钥管理对敏感的玩家数据进行静态加密。 如果残留块泄露,没有密钥的密文毫无用处。通过 horizOn 的 账户绑定云存档管理的玩家存档使用感知修订的写入和客户端冲突解决——但如果你自行托管容器,底层磁盘隔离仍然是你的责任。

时间线:Cloudflare 如何应对

作为参考,以下是一个资源充足的团队在处理关键基础设施漏洞时能有多快:

时间 (UTC) 操作
Sep 4, 15:26 研究人员通过 HackerOne 报告
Sep 4, 18:45 安全事件启动,确认生产环境配置
Sep 4, 21:27 合并运行时修复及复用测试
Sep 4, 23:15 开始部署
Sep 7, 06:13 部署完成,开始清理
Sep 14, 10:50 研究人员确认 PoC 不再有效
Sep 19, 15:03 所有修复前缓存快照已清除

从报告到确认根因不到 3 小时,到合并修复不到 8 小时。整个集群清理耗时 15 天——对于全球基础设施的滚动操作来说很正常。

速度很重要。漏洞报告和修复之间的每一个小时都是可能被利用的一小时。如果你运营自己的容器基础设施,请在需要之前就构建好 runbook。

结论

Cloudflare 容器漏洞在密码学意义上并不是零日漏洞。它是一个已知的存储配置权衡——用安全换性能——不适合多租户工作负载。修复只是一个配置更改加上一次全集群回收。

如果你在共享容器基础设施上运行游戏后端,今天就审计你的 dm-thin 池。对 I/O 遥测数据运行检测脚本。如果你不想自己管理容器磁盘隔离、集群回收和 I/O 遥测管道,horizOn 可以处理身份验证、崩溃报告、云存档、排行榜以及你的游戏所需的其他后端服务——这样你就可以专注于发布游戏,而不是在凌晨 3 点调试生产环境中的存储层安全。


来源:Cloudflare 如何解决 Containers 中的跨租户数据泄露漏洞