Введение
Даже из закрытого бакета файлы могут попасть в общий доступ, если публичный Worker возвращает их каждому запросившему. В этом упражнении вы воспроизведёте такую ошибку с помощью только двух синтетических экспортов, добавите авторизацию приложения с учётом читателя и докажете, что закрытые загрузки остаются защищёнными, а публичная проверка состояния продолжает работать.
Сначала завершите задания по интеграции документов Worker и временному доступу. Эта новая виртуальная машина содержит намеренно небезопасный обработчик, синтетические файлы, Node.js 22.22.0, Wrangler 4.131.1 и Miniflare 4.20260730.0. Вы создадите новый закрытый бакет и временный Worker. Требуются активный R2 и соответствующие разрешения учебной учётной записи; ознакомьтесь с тарифами. Реальные файлы, данные клиентов и собственный домен не нужны. Перед завершением удалите демонстрацию с открытым доступом и её учётные данные.
Подключите бакет приложения
На этом шаге вы авторизуете виртуальную машину и создадите отдельный закрытый бакет для приложения. Авторизация устройства подтверждает вашу учебную учётную запись. Для управления бакетом R2 используется отдельный API-токен, ограниченный этой учётной записью.
Запустите 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
Откройте в собственном браузере страницу авторизации и введите показанный код устройства. Перед подтверждением согласия убедитесь, что выбраны учебная учётная запись и запрошенные области чтения account и user:
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 символа. Команда openssl rand -hex 6 создаёт двенадцать случайных шестнадцатеричных символов, поэтому имя ресурса этого запуска не совпадёт с именем предыдущего. Here-документ записывает стандартный файл конфигурации, а оболочка подставляет в него ваши переменные.
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 и создайте пользовательский токен с именем этого задания. Выдайте разрешение Account → Workers R2 Storage → Edit, а в разделе Account Resources ограничьте его учебной учётной записью, идентификатор которой вы сохранили. Установите короткий срок действия. Не добавляйте другие учётные записи или посторонние разрешения. Этот токен управления предназначен для администрирования бакетов, включая их создание и удаление. В этом практическом задании Worker обращается к объектам R2 через привязку DOCUMENTS.
Однократно вставьте токен в скрытое приглашение виртуальной машины. umask 077 ограничивает доступ к файлу вашим пользователем, а read -s скрывает ввод. Файл использует стандартную переменную токена Wrangler и исключён из 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
Используйте --env-file=.env.management только для команд управления R2; обычная команда whoami по-прежнему проверяет авторизацию устройства виртуальной машины.
Размещайте --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 остаётся отдельными учётными данными, ограниченными этой учётной записью.
Создайте два новых учётных данных приложения для синтетических читателей. Это не API-токены Cloudflare. Заполните два принадлежащих им объекта и опубликуйте предоставленный обработчик с намеренно открытым доступом:
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 запущенным после выполнения задания.
Наблюдайте непреднамеренное публичное скачивание
На этом шаге вы воспроизведёте утечку через Worker, пока сам бакет остаётся закрытым. Binding даёт серверному коду доступ к бакету, но R2 автоматически не решает, каким HTTP-клиентам этот код может доверять.
Скопируйте точный URL развёрнутого Worker и запросите синий экспорт без учётных данных:
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
Он напрямую получает ключ R2 из URL и возвращает его содержимое, не проверяя, принадлежит ли этот файл вызывающему пользователю. В Dashboard проверьте настройки бакета: URL для разработки с публичным доступом отключён, собственные домены не настроены. Эти настройки не закрывают отдельный маршрут Worker. Выполните проверку открытого доступа до замены обработчика.
Реальные настройки показывают отсутствие собственного домена и отключённый публичный URL разработки. Worker с привязкой R2 всё равно может раскрывать данные через свой маршрут.

Реальный запрос в терминале ВМ возвращает HTTP 200 и синтетический синий отчёт без учётных данных. Имя пользователя, префикс и URL Worker приведены как примеры; используйте свой URL развёртывания.

Аутентифицируйте читателя до выбора ключа
На этом шаге вы свяжете аутентификацию — наличие у читателя действительных учётных данных — с авторизацией — правом читателя скачать этот объект. Действительный синий токен не должен позволять получить зелёный экспорт. Аутентифицированная личность задаёт владельца ключа, и владелец в 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
Если секрет отсутствует, он не сможет случайно совпасть с данными вызывающего пользователя. Сравнение дайджестов использует операцию сравнения с защитой от атак по времени выполнения, предоставляемую средой. Проверка состояния остаётся вне защищённого маршрута, а закрытые ответы не могут помещаться в общий кэш. Параметр 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.
Тот же пример 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. Удалите именно эти ключи и сохраните остальные ресурсы учётной записи.
Обновите списки Worker и бакетов в Dashboard и запустите проверку очистки платформы. Ошибки аутентификации или сети не доказывают успешное удаление и считаются неопределённым результатом.
Закройте оставшиеся учётные данные
На этом шаге отзовите токен управления этого задания на странице профиля API Tokens, удалите локальный секрет приложения и закройте авторизацию виртуальной машины. Выполняйте это только после успешного прохождения предыдущей проверки очистки.
rm .env.management .dev.vars
unset BLUE_TOKEN GREEN_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
Убедитесь, что значение loggedIn равно false. Отзыв токена управления — отдельный ручной контрольный пункт в Dashboard; удаление локального файла само по себе не отзывает токен. Не изменяйте обычный вход в Dashboard и токены других заданий.
Резюме
Закройте доступ к файлам через синтетический Worker, свяжите личность читателя с владельцем объекта, сохраните доступ к проверке состояния и убедитесь, что закрытый бакет очищен.



