Le build a livré la clé
Le rapport d'incident étendu de Beacon CRM attribue le vol de l'intégralité de sa base clients, couvrant plus de 1 500 associations britanniques, à une clé d'accès AWS que son propre processus de build avait intégrée dans un fichier JavaScript public. Personne n'a pénétré la machine d'un développeur. Un outil de build a copié une variable d'environnement dans un artefact, ce que font les outils de build. La réponse de Bromure Agentic Coding : la variable ne contient rien qui vaille la peine d'être copié.
Il n'y a pas d'attaquant dans la première moitié de cette histoire. Un processus de build a copié un secret depuis une variable d'environnement vers un fichier JavaScript, et un serveur web a servi ce fichier à quiconque le demandait. Le vol, quand il est venu, était une requête GET.
Beacon est un CRM utilisé par les associations britanniques pour suivre donateurs, soutiens et bénévoles. Le 12 août, son directeur technique, David Simpson, a publié un rapport d'incident étendu sur une brèche que l'entreprise avait divulguée pour la première fois le 4 août. The Register l'a relayé le lendemain, SecurityWeek a suivi le 14 août, et la cause racine tient en une ligne :
Une clé d'accès AWS potentiellement exposée dans des artefacts de build JavaScript publics.
À 01:20:16 UTC le 27 juillet, quelqu'un a commencé à utiliser cette clé, et a conservé cet accès pendant une heure et vingt-sept minutes. L'évaluation de Beacon sur ce qui est parti :
Une copie de la base de données qui contient toutes les données clients de Beacon, y compris les pièces jointes, a été réalisée et probablement téléchargée dans un format lisible par l'acteur malveillant.
Cela couvre plus de 1 500 associations : noms de soutiens, numéros de téléphone, adresses e-mail, adresses postales, historiques de dons et pièces jointes. Aucune donnée bancaire ou de carte, puisque les clients de Beacon n'en stockent pas dedans. L' ICO a examiné au moins une association victime et a conclu qu'elle ne portait aucune responsabilité dans la brèche, ce qui est exact et aussi d'une maigre consolation pour une équipe de collecte de fonds qui doit expliquer à ses soutiens où sont parties leurs adresses personnelles.
Beacon chiffrait les données au repos. Cela n'a rien changé. Cybersecurity News le souligne : AWS déchiffre pour le compte de quiconque détient des identifiants valides. Le chiffrement au repos vous protège contre quelqu'un qui repart avec un disque, et il n'a aucune opinion sur un appelant muni de la clé.
La partie qu'il faut regarder fixement
Lisez le rapport d'incident en cherchant l'intrusion et vous ne la trouverez
pas. Il n'y a eu ni e-mail de phishing, ni mainteneur compromis, ni dépendance
empoisonnée ou injection de prompt, ni malware sur le portable d'un ingénieur.
Rien dans cette histoire n'a contourné un contrôle, parce que personne n'a
extrait l'identifiant de nulle part. Le processus de build de Beacon a copié une
valeur depuis une variable d'environnement dans un bundle, Beacon a déployé le
bundle comme ressource statique, et un serveur web l'a remis à chaque client
qui le demandait, comme prévu. Le canal d'exfiltration était une balise
<script>.
Le mécanisme n'a rien d'exotique. Les builds front-end intègrent les variables d'environnement à dessein, parce que le navigateur n'a aucun environnement à lire à l'exécution :
- Vite substitue toute variable préfixée
VITE_dansimport.meta.envau moment du build. Next.js fait de même pourNEXT_PUBLIC_, et Create React App utilisaitREACT_APP_. Le préfixe est la manière de demander la substitution. - Le
DefinePluginde webpack et le--defined'esbuild remplacent un jeton dans votre source par une chaîne. Ils n'ont aucune notion de quelles chaînes sont secrètes, et aucun moyen d'en acquérir une. - Une source map est une deuxième copie. Les frameworks à rendu serveur ajoutent une troisième voie : une valeur lue dans du code qui finit dans un composant client se retrouve sérialisée dans la payload.
Chacune de ces copies part de l'environnement de qui, ou de quoi, a lancé le build. L'environnement est la matière première, et le build est une machine à le copier dans un fichier que vous publiez ensuite.
Le poste de travail où tout commence
La brèche de Beacon est une histoire de cloud dans ses conséquences. Sa cause siège sur un poste de développeur, et le poste de travail est en train de changer.
Un agent qui traite un ticket fait tout cela en un après-midi ordinaire. Il
ajoute une variable d'environnement pour qu'une fonctionnalité atteigne un
service. Il modifie vite.config.ts ou next.config.js. Il écrit la ligne qui
lit la variable, et il choisit le module dans lequel cette ligne vit, ce qui est
la décision qui détermine si la valeur reste sur le serveur ou passe dans le
bundle. Il lance npm run build. Il commite dist/ quand une cible de déploiement
veut l'artefact versionné.
Rien de tout cela n'exige que l'agent commette une erreur, et vous n'obtenez aucun signal s'il en commet une. Le build réussit, puisqu'une chaîne est une chaîne. Le bundle est livré sur une seule ligne minifiée, donc rien dans le diff n'accroche l'œil. La valeur fonctionne, donc la fonctionnalité fonctionne et le ticket se ferme.
Pendant ce temps, l'espace de travail dans lequel tourne un agent est garni d'identifiants, parce que les identifiants sont ce qui le rend utile : des clés cloud pour qu'il puisse vérifier un bucket, un jeton GitHub pour qu'il puisse pousser une branche, des identifiants de registre et de base de données, une clé d'API de modèle. Chacun d'eux est une chaîne posée dans un environnement qu'un processus de build lit intégralement.
Le rapport de Beacon pose une question directe à cet arrangement. Quelque chose dans votre espace de travail copie votre environnement dans un fichier, tôt ou tard. Demandez-vous ce qu'il y récolte.
Dans un espace de travail Bromure, il récolte une chaîne de région
Bromure Agentic Coding fait tourner l'agent dans une VM Ubuntu jetable sur le framework Virtualization d'Apple, avec un proxy MITM côté hôte comme unique route vers le réseau. La conception des identifiants en découle : les vrais secrets restent sur votre Mac, et la VM reçoit des valeurs qui ont l'air correctes et ne valent rien.
AWS reçoit un traitement à part, parce que SigV4 n'envoie jamais le secret. Le SDK consomme le secret dans le client pour calculer un HMAC et met la signature sur le fil, si bien que Bromure ne peut pas échanger un faux contre une vraie valeur en transit comme il le fait pour un jeton bearer. Il déplace la signature à la place.
Commencez par ce qu'un processus de build trouverait. Bromure exporte
AWS_DEFAULT_REGION et AWS_REGION dans la VM, et aucune clé, aucun secret ni
jeton de session. Le raisonnement figure dans le code source, dans
SessionDisk.swift :
Nous ne devons PAS exporter
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKENici : les variables d'environnement l'emportent surcredential_processdans la chaîne du SDK, et elles saperaient aussi la garantie d'absence de secret sur disque (les variables d'environnement fuient via/proc,ps -Eet l'historique du shell).
Cette liste de canaux de fuite décrit l'inspection de processus. Un bundler qui
lit process.env y a sa place, et la même décision le couvre. Aucun
AWS_SECRET_ACCESS_KEY n'existe dans l'environnement pour qu'un build l'intègre,
donc DefinePlugin ne substitue rien, import.meta.env ne porte rien, et
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY, l'erreur de Beacon commise exprès, publie
une chaîne vide.
Le SDK fonctionne quand même. ~/.aws/config pointe vers un utilitaire
credential_process :
[default]
credential_process = /mnt/bromure-meta/bromure-aws-creds.py
region = eu-west-2
L'utilitaire lit un document JSON sur une socket. Il renvoie le vrai
AccessKeyId, qui vous identifie plutôt qu'il ne vous authentifie, associé à une
SecretAccessKey que Bromure génère pour la session : quarante caractères tirés
de l'alphabet qu'utilise un vrai secret AWS, si bien que boto3, la CLI aws et
Terraform les acceptent et signent comme d'habitude. La signature qu'ils
produisent est vouée à échouer.
Puis l'hôte la corrige. AWSResigner reconnaît toute requête *.amazonaws.com
qui passe par le proxy, retire l'en-tête Authorization de l'invité et
recalcule SigV4 avec des identifiants qui n'existent que dans l'espace
d'adressage du processus hôte, en ajoutant le vrai X-Amz-Security-Token quand
le profil porte du matériel STS. Votre terraform apply fonctionne, et le secret
n'est jamais entré dans la machine qui l'a exécuté.
Contournez le proxy et AWS répond :
An error occurred (InvalidSignatureException) when calling the
ListBuckets operation: The request signature we calculated does not
match the signature you provided.
L'identifiant échoue en position fermée. Un identifiant qui ne fonctionne que routé par votre Mac ne peut pas être exercé depuis le portable d'un inconnu, ce qui est la propriété dont la clé publiée de Beacon manquait.
Le reste du tableau, ce sont des leurres
AWS est le cas particulier. Pour le reste, la frontière du fil fait le travail : chaque identifiant que vous configurez apparaît dans la VM comme un faux qui préserve la structure, dérivé de la vraie valeur plus un sel de 32 octets propre à l'installation, via HKDF-SHA256. La vraie valeur reste chiffrée sur votre Mac, et le proxy la substitue dans la requête une fois les octets sortis de la VM, uniquement quand la requête est destinée à l'hôte pour lequel cet identifiant a été frappé.
Cela résout toute la classe des « un outil a copié l'environnement quelque part où il n'aurait pas dû aller » de la même manière, quel qu'ait été l'outil :
Clés d'API de modèles et de fournisseurs
ANTHROPIC_API_KEY vaut sk-ant-api03-brm-…. OPENAI_API_KEY vaut
sk-brm-…, XAI_API_KEY vaut xai-brm-…. Structure préservée, donc les
CLI les acceptent sans broncher, et inertes partout ailleurs.
Jetons Git, de registre et de cloud
GH_TOKEN vaut ghp_ plus 36 caractères, GitLab glpat- plus 20,
DigitalOcean dop_v1_ plus de l'hexadécimal. ~/.git-credentials,
~/.docker/config.json, ~/.kube/config et
~/.config/doctl/config.yaml sont tous présents, tous remplis, tous faux.
Un préfixe NEXT_PUBLIC_
Préfixez n'importe lequel d'entre eux et le bundler fait ce que vous lui
avez dit. L'artefact publié porte du remplissage en forme de brm-docker-…
ou de ghp_, et l'erreur vous coûte un redéploiement au lieu d'un avis de
divulgation.
Un dist/ ou une source map commités
Même réponse, et elle ne dépend pas de l'attention de quiconque. L'artefact peut siéger dans un dépôt public, être indexé et moissonné par tous les crawlers de secrets en activité, et la chaîne qu'ils récoltent n'authentifie rien.
Bromure dérive les faux de la même façon à chaque fois, si bien qu'un outil qui prend l'empreinte de sa propre clé, comme le fait Claude Code lorsqu'il met en cache un hachage de clé, ne voit jamais l'identifiant changer d'une session à l'autre. Il n'y a pas non plus d'interrupteur à oublier. Le proxy est l'unique route de sortie de la VM, donc une requête qui le contourne porte un substitut et échoue en amont.
Le leurre publié est un fil de détente
La clé de Beacon a séjourné dans un fichier public pendant une durée inconnue avant le 27 juillet, et le premier signal qu'a obtenu quiconque a été un pic dans les rapports AWS Cost and Usage des 27 et 28 juillet, lus après coup.
Le détecteur de compromission de Bromure surveille vos identifiants plutôt que la réputation de la destination, ce qui lui permet de se déclencher sur un hôte que personne n'a jamais nommé. Un automate d'Aho-Corasick construit à partir des faux frappés par l'espace de travail lui-même balaie chaque requête sortante, en-têtes et corps. Un faux à destination d'un hôte hors du périmètre pour lequel il a été frappé compte comme une tentative d'exfiltration, et Bromure réagit :
- Le proxy refuse la requête avec un HTTP 451, et pas un seul octet n'atteint la destination.
- Bromure met la VM en pause sur-le-champ.
- Bromure lève une alerte nommant l'identifiant, l'hôte pour lequel il a été frappé, et l'hôte vers lequel on l'a observé partir.
Bromure marque ensuite l'espace de travail comme compromis, si bien que le lancement suivant exige d'effacer l'image disque de la VM et le home persistant. Vos jetons, vos clés SSH et vos réglages survivent à cela.
Pointez le mécanisme vers le scénario Beacon. Un faux frappé pour une destination qui se présente dans une requête vers une autre est la signature, et peu importe comment le faux est sorti : un bundle fuité, un dotfile dérobé, un inconnu curieux avec un navigateur. La première personne qui essaie l'identifiant qu'elle a trouvé s'annonce d'elle-même, dans un journal qui vous appartient, à l'instant où elle l'essaie.
Le relevé que Beacon dit qu'il n'aura jamais
Simpson clôt le rapport d'incident sur les limites de ce que Beacon peut établir :
Il y a des choses que nous ne serons peut-être jamais en mesure de découvrir sur cet incident.
Beacon énonce la limite sans détour. Les objets précis, la destination exacte des téléchargements et l'attribution définitive de quels objets ont été consultés ne peuvent pas être déterminés à partir des journaux disponibles. La conclusion de Beacon, à savoir que toute la base est partie, est une inférence tirée de la forme d'un rapport de facturation : un volume de transfert dans les données Cost and Usage qui correspond à la taille approximative de ce que Beacon stocke. C'est du bon travail forensique sans rien pour travailler, et chaque association qui écrit en ce moment à ses soutiens s'appuie dessus.
Un espace de travail Bromure produit ce relevé comme sous-produit de son fonctionnement. Chaque requête traverse le proxy hôte, donc le proxy consigne ce qui s'est passé :
$ bromure-cli trace ls
HOST METHOD STATUS MS FLAGS
api.anthropic.com POST 200 412 swap×1
s3.eu-west-2.amazonaws.com GET 200 88
registry.npmjs.org GET 200 31
api.github.com POST 201 140 swap×1
trace hostnames liste chaque hôte distinct qu'une session a contacté, trace summary agrège l'ensemble, et trace leaks montre les identifiants non gérés.
Le Trace Inspector (⇧⌘I) donne la même vue avec les corps de requête, et le
journal de sécurité (Window → Supply Chain Log…) suit en direct les
décisions de chaîne d'approvisionnement et les 451. Bromure conserve tout cela
chiffré au repos sur votre Mac sous la clé maîtresse du coffre.
Les questions auxquelles Beacon ne peut répondre que par inférence sont une recherche que vous exécutez vous-même : à quoi cet espace de travail a-t-il parlé, quand, en transportant quoi, et est-ce que quelque chose en forme d'identifiant est sorti. Contre vos propres données, à la minute où vous vous posez la question.
L'arrangement qui referme l'écart
Les ingénieurs de Beacon n'ont rien fait d'inhabituel. Mettre un identifiant dans une variable d'environnement est la pratique recommandée, et lire des variables d'environnement pendant un build est ce que font les builds. Toute une base de données clients est passée par l'écart entre ces deux choses raisonnables, et aucune dose de soin ne le referme, parce que le soin est un espoir sur l'attention plutôt qu'un contrôle.
Le contrôle consiste à arranger les choses pour que la copie ne vaille rien. Gardez le vrai identifiant sur l'hôte, remettez à l'espace de travail un substitut qui satisfait chaque outil qui le lit, signez et substituez hors de la machine qui exécute le code, et gardez votre propre relevé de chaque requête qui sort. Alors un outil de build qui fait son travail, un agent qui effectue une modification d'apparence raisonnable et un inconnu qui lit votre bundle à loisir arrivent tous au même endroit : une chaîne qui ne signifie rien hors de votre Mac.
Sources : The Register, « AWS key exposed in JavaScript may have lit way to Beacon's charity data » (13 août 2026) · SecurityWeek, « Over 1,000 Charities Hit by Beacon CRM Data Breach » (14 août 2026) · Infosecurity Magazine, « Exposed AWS Access Key Linked to Data Breach Affecting 1500+ UK Charities » · Cybersecurity News, « Beacon CRM Confirms Full Database Theft After AWS Access Key Breach »