Comment cacher une clé API dans le JavaScript frontend sans la divulguer
Le code du navigateur est public par nature : toute clé API intégrée à un bundle React, Vue ou JavaScript classique peut être copiée en quelques secondes. Cet article montre comment un petit proxy serveur, des limites de débit, des jetons restreints et une rotation rapide gardent vos identifiants hors de portée.
Ouvrez un site que vous avez créé le mois dernier, appuyez sur F12 et cliquez sur l’onglet Réseau. Chaque en-tête envoyé par votre JavaScript y apparaît en clair, y compris la valeur Authorization que vous étiez sûr que personne ne trouverait. C’est la vérité désagréable derrière comment cacher une clé API dans le JavaScript frontend : vous ne pouvez pas la cacher là-bas. Ce que vous pouvez faire, c’est ne plus jamais envoyer le secret au navigateur, et laisser un serveur que vous contrôlez effectuer l’appel au nom de vos utilisateurs.
Cet article montre concrètement comment cela fonctionne. Vous verrez pourquoi les bundlers exposent les variables d’environnement, comment des robots trouvent des jetons en quelques minutes après un déploiement, et comment construire un petit proxy en Express ou sur Cloudflare Workers qui garde votre identifiant sur le serveur. Nous ajoutons ensuite des limites de débit, la validation des entrées, des jetons restreints et un plan de réaction propre au cas où une fuite surviendrait malgré tout.
💡 Réponse courte : si un secret est intégré à du code exécuté dans le navigateur, il est public. Pour le cacher, déplacez la requête vers un backend, sans chercher à encoder, découper ou brouiller la chaîne.
Pourquoi le code frontend ne peut pas garder de secrets
Un navigateur fonctionne en téléchargeant votre code et en l’exécutant sur la machine du visiteur. Tout ce qu’il télécharge, le visiteur peut le lire : HTML, CSS, bundles JavaScript, source maps et chaque requête que votre code envoie. Aucun paramètre, aucun indicateur ni aucune étape de compilation ne rend invisible une chaîne pour la personne dont l’ordinateur exécute le code.
Tout est lisible dans le navigateur
Trois endroits exposent un jeton sans aucune compétence en piratage :
L’onglet Réseau. Chaque requête affiche son URL, ses en-têtes et sa charge utile. Un jeton Bearer placé dans un en-tête se trouve à un clic.
L’onglet Sources. Votre bundle est là, et si les source maps sont activées, vos fichiers d’origine le sont aussi, commentaires compris.
Afficher le code source et curl. N’importe qui peut télécharger votre bundle et exécuter grep pour repérer des préfixes de jetons comme sk_ ou pia_sk_.
Les bundlers intègrent vos variables
Une confusion courante ressemble à ceci : « Je l’ai mise dans un fichier .env, donc elle est privée. » Le fichier .env est privé. Ce que votre bundler en fait, c’est une autre histoire. Vite, Next.js et Create React App remplacent, au moment de la compilation, les variables portant un préfixe spécifique par leur valeur littérale.
Framework
Préfixe rendu public
Ce qui se passe
Vite
VITE_
La valeur est intégrée au bundle
Next.js
NEXT_PUBLIC_
La valeur est intégrée au code client
Create React App
REACT_APP_
La valeur est intégrée à la compilation
Nuxt
NUXT_PUBLIC_
La valeur se retrouve dans la configuration d’exécution publique
Ainsi, VITE_PROVIDER_TOKEN=abc123 dans un fichier .env finit comme la chaîne littérale "abc123" dans assets/index-xxxx.js. Les variables sans le préfixe public restent hors du bundle client, ce qui explique précisément pourquoi le secret doit rester côté serveur.
L’obfuscation ne fait que ralentir
Base64, découpage de chaînes, caractères inversés, astuces XOR : rien de tout cela ne fonctionne, car votre code doit reconstruire la vraie valeur avant d’envoyer la requête. Une fois la requête partie, l’onglet Réseau affiche le résultat final. L’obfuscation accorde à un attaquant dix minutes d’agacement et vous offre un casse-tête de maintenance permanent.
Comment les fuites se produisent réellement
Les robots scannent les dépôts et les bundles
La méthode la plus rapide est aussi la plus banale : ouvrir la page, déclencher la fonctionnalité, lire l’en-tête. Aucun script n’est nécessaire. Les scanners automatisés vont plus loin. Ils parcourent les dépôts publics, les paquets npm et les sites en ligne à la recherche de formats de jetons connus, et de nombreux fournisseurs utilisent des préfixes reconnaissables (sk_, ghp_, pia_sk_) précisément pour que les scanners les repèrent.
Un jeton poussé dans un dépôt GitHub public peut être repéré en quelques minutes. Certains fournisseurs scannent et révoquent automatiquement, ce qui est un bonus appréciable, mais cela ne constitue pas un plan.
Ce que coûte réellement une fuite
Sur les API facturées à l’usage, la facture est le dégât visible. Le dégât caché est pire : des quotas épuisés qui mettent votre propre application hors ligne, des abus signalés sur votre compte et, avec des permissions trop larges, l’accès à de vraies données.
Identifiant divulgué
Abus typique
Ce que cela vous coûte
Jeton LLM
Chatbots gratuits, génération de spam
Facture de tokens, blocage par limite de débit
Jeton de génération d’images ou de vidéos
Rendus en masse, revente
Facture d’utilisation GPU
Jeton de cartes ou de recherche
Scraping à grande échelle
Épuisement du quota
Secret de base de données ou de stockage
Lecture ou suppression d’enregistrements
Fuite de données
Les applications d’IA sont la cible favorite. Des modèles comme GPT 5.6 Luna ou Claude Sonnet 5 facturent au token, si bien qu’un identifiant volé se transforme directement en calcul gratuit pour quelqu’un d’autre, à vos frais.
Placer un proxy entre le navigateur et l’API
La solution est architecturale. Au lieu que le navigateur appelle directement le fournisseur, le navigateur appelle votre serveur, et votre serveur appelle le fournisseur.
Browser -> POST /api/generate -> Your server -> Provider API
(holds the secret)
Trois règles gardent cette conception saine :
Le secret ne vit que dans les variables d’environnement du serveur. Jamais dans le dépôt, jamais dans une variable de style NEXT_PUBLIC_.
Le navigateur n’envoie que la saisie de l’utilisateur. Un prompt, un identifiant, un choix dans une liste. Jamais une URL, un en-tête ou un nom de modèle qu’il choisit librement.
Le serveur décide de ce qui est autorisé. Il valide la saisie, ajoute l’identifiant, transmet l’appel et ne renvoie que les champs dont la page a besoin.
Un proxy Express fonctionnel
Cet exemple transmet une requête d’image à l’API PicassoIA, qui s’authentifie avec un jeton Bearer dans l’en-tête Authorization et expose les prédictions à l’adresse /v1/models/{owner}/{name}/predictions. La même structure fonctionne pour n’importe quel autre fournisseur.
// server.js
import express from "express";
import rateLimit from "express-rate-limit";
const app = express();
app.use(express.json({ limit: "20kb" }));
app.use("/api/", rateLimit({ windowMs: 60_000, limit: 10 }));
const UPSTREAM =
"https://api.picassoia.com/v1/models/picassoia/picassoia-image/predictions";
app.post("/api/generate", async (req, res) => {
const { prompt } = req.body ?? {};
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 500) {
return res.status(400).json({ error: "Prompt must be 1 to 500 characters." });
}
try {
const upstream = await fetch(UPSTREAM, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PICASSOIA_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input: { prompt } }),
});
const data = await upstream.json();
// Return only what the browser needs, never the raw upstream body.
res.status(upstream.status).json({ id: data.id, status: data.status });
} catch {
res.status(502).json({ error: "Upstream request failed." });
}
});
app.listen(3000);
Lancez-le avec node --env-file=.env server.js (Node 20.6 ou plus récent) afin que le jeton provienne d’un fichier non suivi par Git. L’API PicassoIA est asynchrone : vous créez une prédiction, puis vous l’interrogez. Ajoutez une seconde route GET /api/result/:id construite de la même façon, et vérifiez les champs exacts de la réponse sur la page de l’API PicassoIA.
Version serverless sur Cloudflare Workers
Pas de serveur à maintenir ? Un Worker fait le même travail en moins de lignes. Stockez le secret avec npx wrangler secret put PICASSOIA_TOKEN et il ne touchera jamais votre dépôt.
Vercel Functions, Netlify Functions et AWS Lambda suivent le même modèle : une petite route, un secret dans les paramètres de la plateforme, aucun identifiant dans le code client.
Le code frontend après la correction
Le navigateur ne parle désormais qu’à votre propre route :
async function generate(prompt) {
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}
Compilez l’application, ouvrez le bundle et cherchez-y. Il ne devrait plus rien trouver.
Verrouillez aussi le proxy
Un proxy sans limites est simplement un moyen plus pratique pour des inconnus de dépenser votre argent. Traitez-le comme un point de terminaison public, car c’en est un.
💡 À propos de CORS : définir Access-Control-Allow-Origin sur votre propre domaine empêche d’autres sites d’appeler votre proxy depuis le navigateur d’un visiteur. Cela ne sert à rien contre curl ou un script. La vraie protection vient de l’authentification, des limites de débit et de la validation.
Limites de débit par utilisateur ou par IP
Limitez par utilisateur authentifié lorsque vous avez des comptes, et par adresse IP lorsque vous n’en avez pas. Derrière un CDN ou un répartiteur de charge, assurez-vous que votre framework lit la véritable IP du client (dans Express, cela signifie configurer correctement trust proxy), sinon tous les visiteurs partageront le même compartiment.
Type de point de terminaison
Limite de départ
Pourquoi
Génération de texte
20 requêtes par minute et par utilisateur
Peu coûteux à l’appel, facile à spammer
Génération d’images
5 requêtes par minute et par utilisateur
Coût GPU réel à chaque appel
Génération de vidéos
2 requêtes par minute et par utilisateur
Lent et coûteux
Vérifications de statut en lecture seule
60 requêtes par minute et par utilisateur
Le polling est normal
Considérez ces chiffres comme des points de départ et ajustez-les en fonction du trafic réel.
Validez chaque entrée
Ne transmettez jamais le corps de la requête du navigateur tel quel. Vérifiez plutôt chaque champ :
Limitez la longueur du prompt. L’exemple ci-dessus rejette tout ce qui dépasse 500 caractères.
Établissez une liste blanche de modèles. Laissez la page envoyer "fast" ou "quality", puis faites correspondre ces libellés aux noms de modèles réels sur le serveur.
Limitez la taille des sorties. Définissez un nombre maximal de tokens de sortie ou d’images par appel.
Rejetez les champs inconnus. Si le schéma prévoit prompt, rien d’autre ne passe.
Fixez des plafonds de budget chez le fournisseur
La plupart des fournisseurs permettent de définir des plafonds de dépenses mensuels et des alertes. Activez-les. C’est le filet de sécurité le jour où toutes les autres couches échouent. Utilisez également un jeton distinct par projet et par environnement, afin que révoquer l’un n’arrête jamais les autres.
Quand un jeton public est acceptable
Tous les identifiants ne sont pas des secrets. Quelques-uns sont conçus pour les navigateurs : la configuration web de Firebase, les jetons publiables de Stripe (ceux qui commencent par pk_) et les jetons JavaScript de Google Maps. Ils ne sont sûrs qu’à condition d’être restreints.
Restreignez par domaine et par périmètre
Ouvrez le tableau de bord du fournisseur et appliquez toutes les restrictions proposées :
Limites de référent HTTP pour que le jeton ne fonctionne que depuis yourdomain.com.
Limites de périmètre API pour qu’un jeton Maps ne puisse appeler que Maps et rien d’autre.
Quotas quotidiens pour qu’une vague d’abus se heurte à un plafond.
Une mise en garde : les vérifications de référent reposent sur l’en-tête Referer, que les clients hors navigateur peuvent falsifier. Les restrictions réduisent les abus occasionnels, mais elles ne transforment pas un jeton public en secret. Gardez la valeur limitée, bien délimitée et dont le budget est plafonné.
Jetons de courte durée pour les navigateurs
Certaines tâches sont trop lourdes pour passer par votre serveur, comme les gros envois de fichiers ou le streaming en temps réel. Pour celles-ci, utilisez un échange de jetons :
L’utilisateur se connecte à votre backend.
Votre backend demande au fournisseur un jeton de courte durée et à périmètre étroit, ou signe un JWT qui expire au bout de 5 à 15 minutes.
Le navigateur utilise directement ce jeton temporaire pour la requête lourde.
Le jeton expire de lui-même, si bien qu’une valeur copiée devient vite inutile.
Plusieurs fournisseurs de temps réel et de stockage proposent des jetons éphémères pour ce schéma précis. Votre secret de longue durée ne quitte jamais le serveur.
Que faire après une fuite
Révoquer d’abord, enquêter ensuite
Si un jeton a été public ne serait-ce qu’une heure, partez du principe que quelqu’un l’a copié. Suivez cette liste dans l’ordre :
Révoquez ou renouvelez le jeton dans le tableau de bord du fournisseur, immédiatement.
Déployez le remplaçant uniquement dans l’environnement du serveur.
Consultez les journaux d’utilisation pour la période d’exposition et recherchez des IP, des modèles ou des pics inconnus.
Resserrez les plafonds de dépenses avant toute autre action.
Prévenez votre équipe et vérifiez si la même valeur a été réutilisée ailleurs.
Nettoyer l’historique Git et les bundles
Supprimer le secret de votre dernier commit ne résout rien, car l’historique le conserve encore. Les anciens déploiements, les caches CDN et les source maps publiques peuvent aussi le contenir. Le renouvellement est la vraie solution. Réécrire l’historique avec git filter-repo relève de l’hygiène, à faire ensuite.
Ensuite, rendez une récidive improbable :
Ajoutez un scanner pré-commit comme gitleaks.
Activez la protection des push et la détection de secrets dans votre hébergeur Git.
Évitez de publier les source maps en production, ou servez-les uniquement à votre outil de suivi des erreurs.
Ajoutez une étape CI qui recherche les préfixes de jetons connus dans le bundle compilé et fait échouer la compilation en cas de correspondance.
Auditer votre bundle avec un LLM
Un LLM est un second regard rapide pour ce travail. Voici comment utiliser Claude Sonnet 5 sur PicassoIA pour repérer les fuites et esquisser votre proxy :
Compilez et analysez d’abord. Exécutez npm run build, puis grep -rE "sk_|pk_|pia_sk_|Bearer " dist/ pour repérer vous-même les cas évidents.
Collez uniquement du code caviardé. Incluez les fichiers qui effectuent des appels réseau, en remplaçant chaque valeur réelle par REDACTED. Ne collez jamais un secret actif dans un outil de discussion.
Posez une question précise. Par exemple : « Liste chaque endroit où ce code envoie un identifiant depuis le navigateur, et réécris chacun pour appeler plutôt une route serveur. »
Vérifiez la réponse selon les règles ci-dessus. Contrôlez la validation, les limites de débit et la gestion des erreurs, puis testez dans les outils de développement.
💡 Astuce : pour un second avis, lancez le même prompt sur GPT 5.6 Sol ou obtenez une première analyse rapide avec Gemini 3.5 Flash. Différents modèles repèrent différentes erreurs.
Créer des applications d’images sans divulguer de jetons
Tout ce qui précède s’applique avec encore plus de force lorsque votre application génère des images ou des vidéos, car chaque appel consomme du temps GPU. Le schéma reste le même : la page recueille un prompt, votre proxy garde l’identifiant, et PicassoIA génère le rendu.
Choisissez un modèle adapté à votre produit. Flux 2 Pro convient au photoréalisme détaillé, Seedream 4.5 gère les prompts comportant de nombreux éléments, P-Image est une option rapide pour les aperçus, et GPT Image 2 excelle pour le texte dans les images. Pour le mouvement, parcourez les modèles de texte vers vidéo dans le catalogue complet de modèles PicassoIA.
Voici une courte liste de contrôle avant votre prochain déploiement :
Aucun identifiant n’apparaît dans le bundle compilé ni dans les source maps.
Le navigateur appelle votre proxy, jamais le fournisseur.
La longueur du prompt, le choix du modèle et la taille de sortie sont validés côté serveur.
Les limites de débit et un plafond de dépenses chez le fournisseur sont actifs.
Tout jeton public est restreint par domaine, périmètre et quota.
Vous connaissez les étapes de révocation de chaque jeton avant d’en avoir besoin.
Prêt à voir le résultat ? Ouvrez PicassoIA, générez quelques images avec les modèles ci-dessus, puis branchez le même prompt sur votre propre proxy. Essayez différents styles et prompts, et publiez une application où la seule chose que les visiteurs peuvent copier depuis le navigateur est l’image finale.