Cerrar una descarga pública no intencionada

CloudflareBeginner
Practicar Ahora

Introducción

Un bucket privado aún puede filtrar archivos si un Worker público los devuelve a cualquier persona que realice una solicitud. En este laboratorio reproducirá ese defecto usando solo dos exportaciones sintéticas, añadirá autorización de la aplicación limitada al lector y demostrará que las descargas privadas siguen protegidas mientras el endpoint público de salud continúa disponible.

Complete primero las lecciones sobre la integración de documentos de Worker y el acceso temporal. Esta VM nueva proporciona un controlador intencionadamente inseguro, archivos sintéticos, Node.js 22.22.0, Wrangler 4.131.1 y Miniflare 4.20260730.0. Usted creará un bucket privado nuevo y un Worker temporal. Se requieren R2 activo y los permisos correspondientes de su cuenta de aprendizaje; consulte precios. No se necesitan archivos reales, datos reales de clientes ni un dominio personalizado. Elimine la demostración expuesta y sus credenciales antes de salir.

Conectar el bucket de la aplicación

En este paso, autorizará esta VM y creará un bucket privado independiente para la aplicación. La autorización del dispositivo confirma su cuenta de aprendizaje. La administración de buckets de R2 utiliza un token de API independiente, restringido a esa cuenta.

Inicie Bash para usar la sintaxis de comandos que se muestra a continuación. Después, vaya al proyecto preparado y compruebe sus herramientas. Mantenga abierto este mismo terminal para conservar disponibles las variables con los nombres de sus recursos:

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

Autorice el código de dispositivo mostrado en su propio navegador. Confirme la cuenta de aprendizaje y los permisos de lectura de cuenta y usuario solicitados antes de conceder el consentimiento:

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json

Exija que aparezca loggedIn: true. Lea el nombre de la cuenta aunque solo aparezca una. Sustituya YOUR_ACCOUNT_ID a continuación por el ID real de 32 caracteres de esa cuenta. openssl rand -hex 6 genera doce caracteres hexadecimales aleatorios para evitar que este laboratorio entre en conflicto con una ejecución anterior. El documento aquí mostrado escribe un archivo de configuración estándar; el shell sustituye en él los valores de sus variables.

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

Para administrar el bucket, abra la página API Tokens de su perfil de Cloudflare y cree un token personalizado con un nombre relacionado con este laboratorio. Conceda Account → Workers R2 Storage → Edit y limite Account Resources a la cuenta de aprendizaje cuyo ID guardó. Configure una caducidad breve. No incluya otras cuentas ni permisos no relacionados. Este token de administración sirve para administrar buckets, incluida su creación y eliminación. En este laboratorio, el Worker accede a los objetos de R2 mediante su binding DOCUMENTS.

Copie el token una sola vez en este prompt oculto de la VM. umask 077 restringe el archivo a su usuario y read -s oculta la entrada. El archivo utiliza la variable de token estándar de Wrangler y queda excluido de 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

Use --env-file=.env.management únicamente para los comandos de administración de R2; whoami seguirá comprobando la autorización del dispositivo de la VM.

Coloque --env-file al final de cada comando de Wrangler para que su lista de archivos no incluya el nombre del comando. Después de crear cada bucket, si Wrangler pregunta si debe añadir un enlace a la configuración, escriba n y pulse Enter. La configuración ya contiene el enlace previsto.

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

Muestre sus buckets y busque el nombre generado exacto. Los demás buckets pertenecen a otros trabajos; no los modifique.

npx wrangler r2 bucket list --env-file=.env.management

En Dashboard, abra Storage & databases → R2 → Overview, seleccione este bucket exacto y compruebe que la lista de objetos está vacía. En su configuración, mantenga desactivados la URL pública de desarrollo y los dominios personalizados. El nombre del bucket en Dashboard confirma su identidad; las comprobaciones de descarga posteriores demostrarán cuáles son los bytes almacenados.

El permiso para los scripts de Worker permite realizar el despliegue. El permiso de KV permite llevar el registro de eliminación de Wrangler; este laboratorio no crea ningún espacio de nombres de KV. El token de administración de R2 sigue siendo una credencial independiente y limitada a la cuenta.

Cree dos credenciales nuevas de aplicación para los lectores sintéticos. No son tokens de API de Cloudflare. Prepare los dos objetos pertenecientes a esos lectores y publique el controlador proporcionado, que está intencionadamente expuesto:

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

Este despliegue expuesto contiene deliberadamente solo estos dos archivos sintéticos. No use exportaciones reales ni lo deje en ejecución después del ejercicio.

Observar la descarga pública no intencionada

En este paso, reproducirá la exposición a través del Worker mientras el bucket subyacente sigue siendo privado. Un binding permite que el código del servidor acceda al bucket; R2 no decide automáticamente en qué solicitantes HTTP debe confiar ese código.

Copie la URL exacta del despliegue y solicite la exportación azul sin credenciales:

BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/exports/blue/report.txt"

Exija HTTP 200 y Synthetic blue export.. Si el nuevo despliegue todavía se está propagando, repita la solicitud durante un máximo de un minuto. Esta respuesta anónima correcta es el defecto que debe reparar.

Inspeccione el controlador proporcionado:

cat src/index.js

El controlador lee directamente de la URL una clave de R2 y devuelve su contenido sin decidir si la persona que realiza la solicitud es propietaria de ese archivo. En Dashboard, inspeccione la configuración del bucket: la URL pública de desarrollo está desactivada y no hay dominios personalizados. Estas opciones no cierran la ruta independiente del Worker. Ejecute la comprobación de exposición antes de sustituir el controlador.

La configuración real no tiene dominio personalizado y mantiene desactivada la URL pública de desarrollo. Un Worker con enlace R2 aún puede exponer datos mediante su propia ruta.

Configuración del bucket privado

Esta solicitud real en la terminal de la VM devuelve HTTP 200 y el informe sintético azul sin credenciales. El usuario, prefijo y URL del Worker son ejemplos; usa tu propia URL de despliegue.

HTTP 200 anónimo antes de reparar

Autenticar al lector antes de seleccionar la clave

En este paso, vinculará la autenticación (qué lector posee una credencial válida) con la autorización (qué lector puede descargar este objeto). Un token azul válido no debe permitir recuperar una exportación verde. La identidad autenticada proporciona el propietario de la clave, y el propietario indicado en la URL debe coincidir antes de consultar R2.

Sustituya el controlador. Las dos credenciales sintéticas representan a los lectores de este ejemplo pequeño; una aplicación real usaría su sesión o proveedor de identidad y registros autorizados de permisos. Nunca acepte un nombre proporcionado por quien realiza la solicitud como prueba de identidad.

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

La ausencia de un secreto no puede coincidir accidentalmente con una solicitud. La comparación de resúmenes utiliza la operación de igualdad segura frente a ataques de temporización del runtime. /health permanece fuera de la ruta protegida y las respuestas privadas no se consideran candidatas para una caché compartida. Un parámetro key en la cadena de consulta no puede sobrescribir la ruta de almacenamiento autenticada.

Despliegue la reparación:

npx wrangler deploy

La comprobación de la plataforma prueba primero un runtime local independiente con credenciales y bytes de objetos sintéticos aleatorios. Una comprobación local correcta resulta útil antes de evaluar la reparación desplegada.

Demostrar el aislamiento de lectores y conservar el acceso de salud

En este paso, probará las rutas permitidas y las denegadas. Cargue las credenciales sintéticas sin mostrarlas y, después, descargue el archivo de cada propietario:

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

Exija que los bytes coincidan exactamente. Ahora pruebe el acceso anónimo, el intento de un lector de entrar en la ruta del otro propietario, una clave inexistente del propio propietario y el endpoint público de salud:

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"

Exija, respectivamente, 401 Unauthorized, 403 Forbidden, 404 Not found y 200 ok. El estado y el cuerpo deben coincidir. La comprobación de la plataforma repite el aislamiento remoto de lectores y verifica que la cuenta seleccionada es propietaria del Worker y de su binding privado de R2.

La misma URL de ejemplo ahora devuelve HTTP 401 Unauthorized a la solicitud anónima en la terminal. Son resultados HTTP de terminal; las comparaciones de bytes autenticadas y las comprobaciones independientes demuestran el aislamiento.

HTTP 401 anónimo tras reparar

Eliminar la aplicación y el bucket remotos

En este paso, eliminará únicamente el Worker y los objetos de este laboratorio mientras mantiene la autorización activa. El bucket privado no desaparece al eliminar su Worker.

npx wrangler delete

Confirme el nombre exacto del Worker generado. Elimine explícitamente el único objeto cargado y, después, elimine el bucket:

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

Solo se crearon las claves de los informes azul y verde. Elimine esas claves exactas y conserve los recursos de cuenta no relacionados.

Actualice las listas de Workers y buckets en Dashboard y ejecute la comprobación de limpieza de la plataforma. Los fallos de autenticación o de red no permiten determinar el resultado; no cuentan como una eliminación correcta.

Revocar las credenciales restantes

En este paso, revoque el token de administración de este laboratorio en la página API Tokens de su perfil, elimine el secreto local de la aplicación y cierre la autorización de la VM. Hágalo únicamente después de que la comprobación de limpieza anterior sea correcta.

rm .env.management .dev.vars
unset BLUE_TOKEN GREEN_TOKEN
npx wrangler logout
npx wrangler whoami --json || true

Exija que aparezca loggedIn: false. La revocación del token de administración es un paso manual independiente en Dashboard; eliminar solo el archivo local no lo revoca. Mantenga intactos el inicio de sesión normal de Dashboard y los tokens de otros laboratorios.

Resumen

Cierre la exposición de archivos de un Worker sintético, vincule la identidad del lector con la propiedad del objeto, mantenga disponible el acceso de salud y verifique la limpieza del bucket privado.