Introduction
Une page d’importation dans un navigateur peut échouer même si une URL pré-signée fonctionne avec curl. Le navigateur vérifie également si le service de stockage autorise l’origine de la page. Vous allez configurer cette règle CORS indépendante pour une page d’importation synthétique fournie, tout en gardant le bucket privé.
Commencez par terminer l’expérience « Autoriser l’accès temporaire aux fichiers ». Cette nouvelle VM fournit Node.js 22.22.0, Wrangler 4.131.1 et AWS SDK 3.888.0, ainsi qu’une structure complète et indépendante pour une page d’importation. Vous allez créer un bucket privé, un identifiant d’objet à courte durée de validité et une page Worker temporaire. R2 doit déjà être activé ; consultez la tarification de R2 et le fonctionnement de CORS. Aucun domaine acheté n’est nécessaire. Utilisez uniquement le texte synthétique fourni ; supprimez toutes les ressources et révoquez les jetons de l’expérience.
Connecter le bucket de l’application
Dans cette étape, vous autorisez cette VM et créez un bucket privé indépendant pour l’application. L’autorisation de l’appareil confirme votre compte d’apprentissage. La gestion du bucket R2 utilise un jeton d’API distinct, limité à ce compte.
Lancez Bash pour utiliser la syntaxe des commandes ci-dessous, puis accédez au projet préparé et vérifiez ses outils. Gardez ce même terminal ouvert afin que les variables contenant les noms de ressources restent disponibles :
bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version
Autorisez le code d’appareil affiché dans votre propre navigateur. Vérifiez le compte d’apprentissage ainsi que les autorisations de lecture du compte et de l’utilisateur demandées avant d’accepter :
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json
Vérifiez que la valeur loggedIn est true. Lisez le nom du compte, même si un seul compte est affiché. Remplacez YOUR_ACCOUNT_ID ci-dessous par l’identifiant réel de 32 caractères de ce compte. openssl rand -hex 6 génère douze caractères hexadécimaux aléatoires afin d’éviter tout conflit avec une exécution précédente. Le document here-doc écrit un fichier de configuration standard ; le shell y remplace vos variables.
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r05-$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
Pour gérer le bucket, ouvrez la page API Tokens de votre profil Cloudflare et créez un jeton personnalisé portant le nom de cette expérience. Accordez l’autorisation Account → Workers R2 Storage → Edit et limitez Account Resources au compte d’apprentissage dont vous avez enregistré l’identifiant. Définissez une courte durée d’expiration. N’incluez pas d’autres comptes ni d’autorisations sans rapport avec cette expérience. Cette autorisation au niveau du compte permet de créer et de supprimer des buckets ; le jeton limité aux objets de l’étape suivante ne le permet pas.
Copiez le jeton une seule fois dans l’invite masquée de cette VM. umask 077 limite l’accès au fichier à votre utilisateur ; read -s masque la saisie. Le fichier utilise la variable de jeton standard de Wrangler et est exclu 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
Utilisez --env-file=.env.management uniquement pour les commandes de gestion R2 ; la commande whoami standard continue de vérifier l’autorisation de l’appareil de la VM.
Placez --env-file à la fin de chaque commande Wrangler pour que sa liste de fichiers ne soit pas étendue au nom de la commande. Après la création de chaque compartiment, si Wrangler propose d’ajouter une liaison à la configuration, saisissez n et appuyez sur Entrée. La configuration contient déjà la liaison prévue.
npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management
Listez vos buckets et recherchez le nom généré exact. Les autres buckets appartiennent à d’autres travaux ; ne les modifiez pas.
npx wrangler r2 bucket list --env-file=.env.management
Dans le Dashboard, ouvrez Storage & databases → R2 → Overview, sélectionnez ce bucket exact et vérifiez que la liste des objets est vide. Dans ses paramètres, laissez l’URL publique de développement et les domaines personnalisés désactivés. Le nom du bucket dans le Dashboard confirme son identité ; les vérifications de téléchargement ultérieures prouveront que les octets sont bien stockés.
L’autorisation du script Worker permet le déploiement. L’autorisation KV permet à Wrangler de suivre les suppressions ; cette expérience ne crée aucun espace de noms KV. Le jeton de gestion R2 reste un identifiant distinct limité au compte.
Préparer la page d’importation et l’accès à la signature
Dans cette étape, vous déployez la page fournie et donnez au terminal un identifiant limité à ce bucket. La page contient uniquement une charge utile synthétique fixe et un champ d’URL vide ; elle ne reçoit jamais le secret de signature à longue durée de validité.
L’API compatible S3 permet aux SDK de stockage standard d’accéder à R2. Elle utilise une paire de clés d’accès distincte du jeton d’appareil de Wrangler. Dans R2 Overview, utilisez Account Details → API Tokens → Manage, puis créez un User API token portant le nom de la ressource générée pour cette expérience. Choisissez Object Read & Write, limitez le jeton à ce nouveau bucket exact et sélectionnez une courte durée d’expiration si le formulaire le propose. Ne choisissez pas tous les buckets ni l’accès Admin. Gardez cette page de jeton ouverte jusqu’à ce que vous ayez enregistré le secret à usage unique.
Utilisez les invites Bash suivantes dans la VM. read -s masque la saisie ; umask 077 rend le fichier d’identifiants lisible uniquement par votre utilisateur. Ces noms sont les variables d’environnement standard de l’AWS SDK. Collez l’Access Key ID et la Secret Access Key dans les invites correspondantes, puis appuyez sur Entrée. Ne collez pas la valeur du jeton d’API général.
umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
Écrivez un client SDK standard réutilisable. Le SDK exige une chaîne de région ; R2 utilise auto. La lecture de la configuration existante garantit que les opérations de l’interface de ligne de commande et du SDK ciblent le même compte et le même bucket.
cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
region: "auto",
requestChecksumCalculation: "WHEN_REQUIRED",
endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
JS
requestChecksumCalculation: "WHEN_REQUIRED" évite de signer une somme de contrôle de corps vide avant que le navigateur fournisse le contenu. L’URL signe toujours l’objet et l’opération ; l’appel de signature ci-dessous inclut explicitement Content-Type. La vérification finale compare les octets stockés.
Publiez la page fournie :
npx wrangler deploy
Copiez l’URL HTTPS du déploiement dans ORIGIN, sans chemin ni barre oblique finale. Une origin se compose du schéma, du nom d’hôte et du port ; https://example.com est différent de http://example.com. Enregistrez cette valeur pour la vérification indépendante ultérieure :
ORIGIN=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
printf "%s\n" "$ORIGIN" > origin.txt
Ouvrez cette URL dans votre navigateur. Vérifiez que la page Synthetic export upload s’affiche avec un champ d’URL temporaire vide. Ne lancez pas encore l’importation ; le bucket n’autorise pas encore cette origine du navigateur.
Autoriser une seule origine de navigateur
Dans cette étape, vous configurez le Cross-Origin Resource Sharing (CORS). Avant d’envoyer une requête PUT inter-origines, un navigateur demande au bucket si l’origine, la méthode et les en-têtes de cette page sont autorisés. Cette requête OPTIONS est une requête de prévalidation (preflight). CORS contrôle l’accès du navigateur ; il ne remplace pas l’autorisation de stockage de l’opération signée.
Écrivez une règle avec l’origine réelle de la page. Le terminal remplace $ORIGIN dans le fichier JSON. Wrangler utilise le format rules/allowed en minuscules de Cloudflare, et non le format CORSRules d’AWS :
cat > cors.json <<JSON
{"rules":[{"allowed":{"origins":["$ORIGIN"],"methods":["PUT"],"headers":["content-type"]},"exposeHeaders":["ETag"],"maxAgeSeconds":60}]}
JSON
La page fournie n’a besoin que de PUT et de Content-Type. exposeHeaders permet au code JavaScript de lire l’ETag renvoyé, et la valeur courte de maxAgeSeconds réduit la durée de mise en cache des requêtes de prévalidation pendant les tests.
npx wrangler r2 bucket cors set "$BUCKET" --file cors.json --env-file=.env.management
npx wrangler r2 bucket cors list "$BUCKET" --env-file=.env.management
Confirmez que vous remplacez la règle uniquement pour ce bucket nouvellement créé. La liste résultante doit afficher votre origine exacte. Dans les paramètres du bucket du Dashboard, consultez la règle CORS en lecture seule et laissez l’accès public désactivé.

Exemple : la politique autorise cette origine, PUT et content-type. Votre nom d’hôte généré sera différent. La sortie CLI vérifie aussi l’exposition d’ETag et la durée du cache.
Importer depuis le navigateur
Dans cette étape, vous combinez l’autorisation PUT signée avec la règle d’origine du navigateur. Le signataire présenté précédemment crée une URL pour uploads/browser.txt exactement ; la page envoie le même Content-Type que celui utilisé lors de la signature.
cat > sign-upload.mjs <<'JS'
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const url = await getSignedUrl(s3, new PutObjectCommand({
Bucket, Key: "uploads/browser.txt", ContentType: "text/plain"
}), { expiresIn: 300, signableHeaders: new Set(["content-type"]) });
writeFileSync("put-url.txt", url, { mode: 0o600 });
console.log("Prepared a five-minute PUT URL");
JS
node --env-file=.env.s3 sign-upload.mjs
Affichez ce lien temporaire uniquement le temps nécessaire pour le copier dans la page fournie. Ne le partagez pas, ne l’incluez pas dans des captures d’écran et ne collez pas le secret S3 à la place :
cat put-url.txt
Collez le lien dans Temporary PUT URL, puis sélectionnez Upload synthetic text dans les cinq minutes. Vérifiez que le résultat HTTP 2xx est bien renvoyé et qu’un ETag est visible. Effacez le champ d’URL après l’importation. Une URL expirée peut apparaître comme une erreur CORS générique du navigateur, car les réponses d’erreur peuvent ne pas contenir d’en-têtes CORS ; générez un nouveau lien si nécessaire.
Téléchargez l’objet réel avec Wrangler et comparez la charge utile fixe :
npx wrangler r2 object get "$BUCKET/uploads/browser.txt" --remote --file browser-download.txt --env-file=.env.management
printf "Synthetic browser export.\n" > expected-browser.txt
cmp expected-browser.txt browser-download.txt
La vérification de la plateforme lit indépendamment les octets et le type de contenu. L’interaction réelle avec le navigateur reste nécessaire comme preuve pour cette expérience ; curl seul n’applique pas les règles CORS du navigateur.

Le navigateur réel a renvoyé HTTP 200 et un ETag lisible ; le champ URL temporaire a été vidé. La page affiche littéralement \n, mais envoie un vrai saut de ligne. Le téléchargement indépendant confirme le contenu exact de 26 octets.
Distinguer la règle d’origine de l’autorisation par signature
Dans cette étape, vous examinez une requête de prévalidation autorisée et une autre refusée, sans créer un nouvel objet. Ces requêtes curl demandent ce que le navigateur est autorisé à faire ; curl n’applique pas lui-même cette réponse.
ACCOUNT_ID=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).account_id")
OBJECT_URL="https://$ACCOUNT_ID.r2.cloudflarestorage.com/$BUCKET/uploads/browser.txt"
curl -i -X OPTIONS -H "Origin: $ORIGIN" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"
Vérifiez que Access-Control-Allow-Origin correspond à l’origine exacte de la page. Effectuez maintenant la requête depuis une origine sans rapport :
curl -i -X OPTIONS -H "Origin: https://outside.example" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "$OBJECT_URL"
La réponse ne doit pas autoriser cette origine, ni *. Le statut exact d’une réponse de refus peut varier ; c’est l’absence d’autorisation qui empêche l’accès du navigateur. Enfin, essayez de lire l’objet sans signature :
curl -sS -o unsigned.xml -w "%{http_code}\n" "$OBJECT_URL"
Exigez 400 avec le Code XML InvalidArgument et le Message Authorization, observés pour une requête sans signature vers ce point de terminaison S3 de R2. Vérifiez les deux champs ; une erreur quelconque ou réseau ne prouve pas un rejet de signature.
python3 - <<'PYXML'
from xml.etree import ElementTree
root = ElementTree.parse("unsigned.xml").getroot()
print("Code:", root.findtext("Code"))
print("Message:", root.findtext("Message"))
PYXML
L’activation de CORS n’a rendu le bucket ni public ni exempt de vérification de signature. Un client qui n’est pas un navigateur et qui possède une signature valide peut toujours utiliser l’opération accordée, quel que soit l’en-tête Origin ; les chaînes d’origine ne sont pas des identifiants.
Supprimer les ressources et les identifiants d’importation
Dans cette étape, vous supprimez le Worker d’interface, l’objet exact et le bucket. Vérifiez l’absence des ressources avant de révoquer les identifiants.
npx wrangler delete
npx wrangler r2 object delete "$BUCKET/uploads/browser.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management
Vérifiez qu’il ne reste que les noms générés par cette expérience. Actualisez la liste des Workers et celle des buckets dans le Dashboard. Exécutez la vérification de nettoyage de la plateforme tant que l’autorisation de gestion reste active. La configuration CORS du bucket est supprimée avec celui-ci.
Révoquer l’identifiant de l’expérience et se déconnecter
Dans cette étape, vous fermez les accès laissés par cet exercice. Dans la page R2 API Tokens, révoquez uniquement le jeton d’objet nommé pour cette expérience. Dans la page API Tokens de votre profil, révoquez le jeton de gestion R2 distinct créé pour cette expérience. La suppression d’un bucket ne révoque pas un jeton, et la déconnexion de Wrangler ne révoque pas les identifiants S3.
Après la révocation, supprimez le fichier d’identifiants local et déconnectez cette VM :
rm .env.s3 .env.management put-url.txt
npx wrangler logout
Examinez l’identité structurée. Son statut non nul est attendu lorsque vous êtes déconnecté :
npx wrangler whoami --json || true
Vérifiez que loggedIn: false ; conservez votre session de connexion habituelle au Dashboard. La plateforme vérifie la suppression des identifiants locaux et la déconnexion de Wrangler. Les deux révocations de jetons sont des points de contrôle manuels dans le Dashboard pour cette version de l’expérience ; elles ne sont pas déduites de la suppression des fichiers.
Résumé
Configurer un CORS avec une origine exacte, importer des octets synthétiques avec une URL pré-signée, distinguer l’autorisation du navigateur de l’autorisation d’accès, puis effectuer le nettoyage.



