介绍
即使存储桶是私有的,只要公开的 Worker 将文件返回给每个请求者,文件仍可能泄露。你将仅使用两个模拟导出文件重现这一缺陷,添加按读取者范围控制的应用授权,并证明私有下载仍受保护,同时公开的健康检查仍可访问。
请先完成 Worker 文档集成和临时访问相关实验。本次全新的 VM 提供了一个故意不安全的处理程序、模拟文件、Node.js 22.22.0、Wrangler 4.131.1 和 Miniflare 4.20260730.0。你将创建一个新的私有存储桶和临时 Worker。必须启用 R2,并拥有相应学习账户的权限;请查看 定价。不需要真实文件、真实客户数据或自定义域名。离开实验前,请清理暴露的演示资源及其凭据。
连接应用存储桶
在此步骤中,你将为此 VM 授权,并为应用创建一个独立的私有存储桶。设备授权用于确认你的学习账户。R2 存储桶管理使用单独的 API token,并限制在该账户内。
先启动 Bash,以使用下面的命令语法,然后进入准备好的项目并检查工具版本。保持此终端打开,以便资源名称变量继续有效:
bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version
在你自己的浏览器中授权显示的设备代码。授予同意前,确认学习账户,以及请求的账户和用户读取权限范围:
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json
要求输出中的 loggedIn: true。即使列表中只有一个账户,也要读取账户名称。将下面的 YOUR_ACCOUNT_ID 替换为该账户实际的 32 位 ID。openssl rand -hex 6 会生成 12 个随机十六进制字符,因此本实验不会与之前的运行发生名称冲突。此处文档会写入一个标准配置文件;Shell 会将你的变量替换到文件中。
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r08-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON
要管理存储桶,请打开 Cloudflare 个人资料中的 API Tokens 页面,并创建一个以本实验命名的自定义 token。授予 Account → Workers R2 Storage → Edit 权限,并将 Account Resources 限制为你之前保存其 ID 的学习账户。设置较短的过期时间。不要包含其他账户或无关权限。此管理令牌用于管理存储桶,包括创建和删除。在本实验中,Worker 通过 DOCUMENTS 绑定访问 R2 对象。
将 token 一次性复制到此隐藏 VM 提示符中。umask 077 会将文件权限限制为当前用户;read -s 会隐藏输入内容。该文件使用 Wrangler 的标准 token 变量,并且会被 Git 排除。
umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN
仅对 R2 管理命令使用 --env-file=.env.management;普通的 whoami 仍会检查 VM 的设备授权。
将 --env-file 放在每条 Wrangler 命令的末尾,避免它的文件参数列表把命令名称也当作参数。创建每个存储桶后,如果 Wrangler 询问是否向配置添加绑定,请输入 n 并按 Enter。配置中已经包含所需的绑定。
npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management
列出你的存储桶并找到准确生成的名称。其他存储桶属于其他工作,请不要修改。
npx wrangler r2 bucket list --env-file=.env.management
在 Dashboard 中打开 Storage & databases → R2 → Overview,选择这个准确的存储桶,并检查其中为空的对象列表。在其设置中保持公开开发 URL 和自定义域名处于禁用状态。Dashboard 中显示的存储桶名称可以确认资源身份;后续下载检查则用于证明其中存储的字节内容。
Worker 脚本权限支持部署。KV 权限支持 Wrangler 的删除记录处理;本实验不会创建 KV 命名空间。R2 管理 token 仍是一个单独的、限定账户范围的凭据。
为模拟读取者创建两个新的应用凭据。这些凭据不是 Cloudflare API token。写入两个归属明确的对象,并发布提供的、故意暴露的处理程序:
umask 077
printf "BLUE_TOKEN=%s\nGREEN_TOKEN=%s\n" "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .dev.vars
npx wrangler r2 object put "$BUCKET/exports/blue/report.txt" --remote --file blue.txt --content-type text/plain --env-file=.env.management
npx wrangler r2 object put "$BUCKET/exports/green/report.txt" --remote --file green.txt --content-type text/plain --env-file=.env.management
npx wrangler deploy
npx wrangler secret bulk .dev.vars
此故意暴露的部署只包含这两个模拟文件。不要使用真实导出文件,也不要在实验结束后继续运行它。
观察意外的公开下载
在此步骤中,你将通过 Worker 重现文件暴露,同时底层存储桶仍保持私有。绑定会让服务器端代码访问存储桶;R2 不会自动判断应该信任哪些 HTTP 请求者。
复制准确的部署 URL,并在不提供凭据的情况下请求 blue 导出文件:
BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/exports/blue/report.txt"
要求 HTTP 状态码为 200,并且响应内容包含 Synthetic blue export.。如果新部署仍在传播,请在最多一分钟内重试读取请求。这个匿名请求成功返回内容的现象,就是需要修复的缺陷。
检查提供的处理程序:
cat src/index.js
它直接从 URL 读取 R2 键,并在未判断请求者是否拥有该文件的情况下返回对象内容。在 Dashboard 中检查存储桶设置:公开开发 URL 已禁用,且没有自定义域名。这些设置并不能关闭单独的 Worker 路由。替换处理程序前,先运行暴露检查。
真实桶设置显示没有自定义域名,公共开发 URL 也已禁用。具有 R2 绑定的 Worker 仍可能通过自己的应用路由公开数据。

真实 VM 终端请求在未提供凭据时返回 HTTP 200 和蓝队合成报告。用户名、资源前缀及 Worker URL 仅为示例;请使用你自己的部署 URL。

在选择键之前验证读取者身份
在此步骤中,你将把身份验证(读取者是否持有有效凭据)与授权(读取者是否可以下载此对象)关联起来。有效的 blue token 不能获取 green 导出文件。经过验证的身份会提供对象所有者,只有当 URL 中的所有者与该身份一致时,才会查询 R2。
替换处理程序。此示例使用两个模拟凭据来代表读取者;实际应用会使用自身的会话或身份提供商,以及权威的权限记录。绝不要接受请求者提供的名称并将其作为身份凭证。
cat > src/index.js <<'JS'
async function matches(actual, secret) {
if (!secret) return false;
const expected = `Bearer ${secret}`;
const a = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(actual));
const b = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(expected));
return crypto.subtle.timingSafeEqual(a, b);
}
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path === "/health" && request.method === "GET") return new Response("ok");
const actual = request.headers.get("Authorization") || "";
const owner = await matches(actual, env.BLUE_TOKEN) ? "blue" : await matches(actual, env.GREEN_TOKEN) ? "green" : null;
if (!owner) return new Response("Unauthorized", { status: 401 });
if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
const route = /^\/exports\/(blue|green)\/([a-z0-9-]+\.txt)$/.exec(path);
if (!route) return new Response("Not found", { status: 404 });
if (route[1] !== owner) return new Response("Forbidden", { status: 403 });
// The authenticated owner and validated path determine the storage key.
const key = `exports/${owner}/${route[2]}`;
const object = await env.DOCUMENTS.get(key);
if (!object) return new Response("Not found", { status: 404 });
const headers = new Headers({ "Cache-Control": "private, no-store" });
object.writeHttpMetadata(headers);
headers.set("ETag", object.httpEtag);
return new Response(object.body, { headers });
}
};
JS
缺少 secret 时不能意外匹配请求者。摘要比较使用运行时提供的抗时序攻击相等操作。健康检查路径不受受保护下载路径的限制,私有响应也不会被视为共享缓存候选项。查询字符串中的 key 不能覆盖经过身份验证后生成的存储路径。
部署修复后的代码:
npx wrangler deploy
平台检查会先在单独的本地运行时中,使用随机生成的模拟凭据和对象字节进行测试。部署修复前通过本地检查,有助于确认代码状态。
验证读取者隔离和健康检查保留
在此步骤中,你将测试允许和拒绝的请求路径。加载模拟凭据,但不要显示其内容,然后下载每个所有者的文件:
set -a
source .dev.vars
set +a
curl -fsS -H "Authorization: Bearer $BLUE_TOKEN" "$BASE_URL/exports/blue/report.txt" -o blue-download.txt
cmp blue.txt blue-download.txt
curl -fsS -H "Authorization: Bearer $GREEN_TOKEN" "$BASE_URL/exports/green/report.txt" -o green-download.txt
cmp green.txt green-download.txt
要求文件字节完全一致。现在测试匿名访问、读取者访问另一个所有者的路径、访问属于自己的缺失键,以及公开健康检查:
curl -i "$BASE_URL/exports/blue/report.txt"
curl -i -H "Authorization: Bearer $BLUE_TOKEN" "$BASE_URL/exports/green/report.txt"
curl -i -H "Authorization: Bearer $BLUE_TOKEN" "$BASE_URL/exports/blue/missing.txt"
curl -i "$BASE_URL/health"
要求这些请求分别返回 401 Unauthorized、403 Forbidden、404 Not found 和 200 ok。状态码和响应正文必须相互一致。平台检查会再次测试远程读取者隔离,并验证所选账户拥有该 Worker 及其私有 R2 绑定。
在 VM 终端中,同一个示例 Worker URL 现在向匿名请求返回 HTTP 401 Unauthorized。这些截图记录的是终端 HTTP 结果;带身份认证的字节比较和独立检查证明读者隔离。

删除远程应用和存储桶
在此步骤中,你仍处于授权状态,只删除本实验的 Worker 和对象。删除 Worker 后,私有存储桶不会自动消失。
npx wrangler delete
确认准确生成的 Worker 名称。先明确删除上传的对象,再删除存储桶:
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/exports/blue/report.txt" --remote --env-file=.env.management
npx wrangler r2 object delete "$BUCKET/exports/green/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
本实验只创建了 blue 和 green 两个报告键。删除这两个准确的键,保留账户中的其他资源。
在 Dashboard 中刷新 Worker 列表和存储桶列表,并运行平台清理检查。身份验证或网络失败不能证明删除成功,只能视为结果不确定。
删除剩余凭据
在此步骤中,请在个人资料的 API Tokens 页面撤销本实验的管理 token,删除本地应用 secret,并关闭 VM 授权。只有在上一步的清理检查通过后,才能执行这些操作。
rm .env.management .dev.vars
unset BLUE_TOKEN GREEN_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
要求输出中的 loggedIn: false。撤销管理 token 是 Dashboard 中单独的手动检查点;仅删除本地文件并不会撤销该 token。保留普通的 Dashboard 登录状态,以及其他实验使用的 token,不要修改它们。
总结
关闭模拟的 Worker 文件暴露,将读取者身份与对象所有权关联起来,保留健康检查访问,并验证私有存储桶已清理。



