소개
비공개 버킷이라도 공개 Worker 가 모든 요청자에게 파일을 반환하면 파일이 유출될 수 있습니다. 이 실습에서는 두 개의 합성 내보내기 파일만 사용해 해당 결함을 재현하고, 리더 범위의 애플리케이션 인증을 추가합니다. 그런 다음 공개 상태 확인 기능은 계속 사용할 수 있으면서 비공개 다운로드는 보호되는지 검증합니다.
먼저 Worker 문서 통합 및 임시 액세스 실습을 완료합니다. 이 새 VM 에는 의도적으로 안전하지 않은 핸들러, 합성 파일, Node.js 22.22.0, Wrangler 4.131.1, Miniflare 4.20260730.0 이 준비되어 있습니다. 새 비공개 버킷과 일회용 Worker 를 생성합니다. 활성화된 R2 와 해당 학습 계정 권한이 필요하므로 가격 정책을 확인합니다. 실제 파일, 실제 고객 데이터 또는 사용자 지정 도메인은 필요하지 않습니다. 실습을 종료하기 전에 노출된 데모와 해당 자격 증명을 정리합니다.
애플리케이션 버킷 연결
이 단계에서는 이 VM 을 인증하고 애플리케이션용 독립 비공개 버킷을 생성합니다. 디바이스 인증은 학습 계정을 확인합니다. 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 자 ID 로 바꿉니다. openssl rand -hex 6은 임의의 16 진수 문자 12 개를 생성하므로 이전 실행과 리소스 이름이 충돌하지 않습니다. 여기 문서는 표준 구성 파일을 작성하며, 셸이 변수 값을 파일에 삽입합니다.
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 는 DOCUMENTS 바인딩을 통해 R2 객체에 접근합니다.
토큰을 한 번만 복사하여 이 숨겨진 VM 프롬프트에 입력합니다. 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
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 관리 토큰은 별도의 계정 범위 자격 증명으로 유지됩니다.
합성 리더를 위한 새로운 애플리케이션 자격 증명 두 개를 생성합니다. Cloudflare API 토큰이 아닙니다. 소유 객체 두 개를 저장하고, 제공된 의도적으로 노출된 핸들러를 배포합니다.
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.가 반환되는지 확인합니다. 새 배포가 아직 전파되지 않았다면 최대 1 분 동안 다운로드를 다시 시도합니다. 인증되지 않은 요청이 성공하는 것이 바로 수정해야 할 결함입니다.
제공된 핸들러를 확인합니다.
cat src/index.js
이 핸들러는 URL 에서 R2 키를 직접 읽고, 요청자가 해당 파일의 소유자인지 확인하지 않은 채 본문을 반환합니다. Dashboard 에서 버킷 설정도 확인합니다. 공개 개발 URL 은 비활성화되어 있고 사용자 지정 도메인도 없어야 합니다. 이러한 설정만으로는 별도의 Worker 경로가 차단되지 않습니다. 핸들러를 교체하기 전에 노출 확인을 실행합니다.
실제 설정에는 사용자 지정 도메인이 없고 공개 개발 URL 도 비활성화되어 있습니다. 그래도 R2 바인딩이 있는 Worker 는 자체 경로를 통해 데이터를 노출할 수 있습니다.

실제 VM 터미널 요청은 인증 정보 없이 HTTP 200 과 파란 팀의 합성 보고서를 반환합니다. 사용자 이름, 리소스 접두사와 Worker URL 은 예시이므로 자신의 배포 URL 을 사용하세요.

키를 선택하기 전에 리더 인증
이 단계에서는 인증(유효한 자격 증명을 가진 리더가 누구인지) 과 권한 부여(해당 리더가 객체를 다운로드할 수 있는지) 를 연결합니다. 유효한 blue 토큰으로 green 내보내기 파일을 가져올 수 없어야 합니다. 인증된 ID 가 키의 소유자를 결정하며, R2 를 조회하기 전에 URL 의 소유자와 일치하는지 확인합니다.
핸들러를 교체합니다. 두 개의 합성 자격 증명은 이 작은 예제에서 리더를 모델링한 것입니다. 실제 애플리케이션에서는 세션이나 ID 공급자와 신뢰할 수 있는 권한 레코드를 사용합니다. 호출자가 제공한 이름을 ID 의 증거로 절대 사용하지 않습니다.
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 바인딩을 소유하는지 검증합니다.
같은 예시 Worker URL 이 이제 VM 터미널의 익명 요청에 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 페이지에서 이 실습의 관리 토큰을 폐기하고, 로컬 애플리케이션 시크릿을 삭제한 뒤 VM 인증을 종료합니다. 이전 정리 확인이 통과한 후에만 실행합니다.
rm .env.management .dev.vars
unset BLUE_TOKEN GREEN_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
loggedIn: false인지 확인합니다. 관리 토큰 폐기는 Dashboard 에서 별도로 수동 확인해야 합니다. 로컬 파일만 삭제해도 토큰이 폐기되지는 않습니다. 일반 Dashboard 로그인과 다른 실습의 토큰은 그대로 둡니다.
요약
합성 Worker 파일 노출을 차단하고, 리더 ID 를 객체 소유권에 연결하며, 상태 확인 액세스를 유지하고, 비공개 버킷 정리를 검증합니다.



