Modèle de menaces
Cette page recense les menaces qui pèsent sur BlockLucky, classées selon la méthode STRIDE (usurpation, altération, répudiation, divulgation, déni de service, élévation de privilèges). Pour chaque menace, elle indique la protection en place dans le dépôt, le fichier qui la met en œuvre et le risque qui reste. Les pages suivantes de cette partie détaillent chaque protection. Les notes de probabilité et d'impact sont tenues dans le registre des risques.
Le modèle décrit le dépôt tel qu'il est : un pilote sur réseau de test (Hardhat local ou Sepolia), avec des ethers sans valeur. Plusieurs risques acceptés ici deviendraient bloquants avec des fonds réels. La page Crypto-actifs et MiCA en tire les conséquences.
Périmètre et actifs
| Actif | Où il vit | Pourquoi il compte |
|---|---|---|
| Clés privées des portefeuilles citoyens | Colonne User.walletEncryptedKey, chiffrée en AES-256-GCM |
Elles signent les achats, les cadeaux et les retraits de gains |
Clé du compte opérateur RELAYER_PRIVATE_KEY |
Variable d'environnement du backend et du worker | Elle verse la dotation de bienvenue et porte des rôles du contrat |
Clé de chiffrement ENCRYPTION_KEY |
Variable d'environnement | Elle protège toutes les clés citoyennes, les secrets TOTP et les pièces d'identité |
Secret de signature JWT_SECRET |
Variable d'environnement | Il permet de fabriquer n'importe quelle session |
Secret WEBHOOK_SECRET |
Variable d'environnement | Il authentifie les événements blockchain entrants |
| Cagnotte et commission | Solde du contrat BlockLucky |
Ce sont les seuls fonds en jeu |
| Équité du tirage | Contrat et oracle Chainlink VRF | C'est la promesse centrale du projet |
| Données personnelles | PostgreSQL (email, historique) et Redis (préférences, codes) | Elles relèvent du RGPD |
| Pièces d'identité en attente | Table IdentityDocument, recto et verso chiffrés en AES-256-GCM jusqu'à la décision |
Donnée la plus sensible du système : une fuite expose l'identité civile du citoyen |
| Mots de passe | Colonne User.passwordHash, empreinte bcrypt |
Souvent réutilisés par les citoyens sur d'autres services |
| Comptes ADMIN et PARTNER | Table User, colonne role |
Ils donnent accès au tirage, à la trésorerie et aux codes promo |
Frontières de confiance
flowchart LR
C["Navigateur et application mobile"] -->|"HTTPS, jeton Bearer"| API["API Express"]
API --> PG[("PostgreSQL")]
API --> RD[("Redis")]
W["Worker BullMQ"] --> PG
W --> RD
API -->|"JSON-RPC signé"| RPC["Nœud RPC Ethereum"]
W -->|"JSON-RPC signé"| RPC
RPC --> SC["Contrat BlockLucky"]
SC -->|"requestRandomWords"| VRF["Coordinateur Chainlink VRF"]
VRF -->|"fulfillRandomWords"| SC
EXT["Émetteur de webhook"] -->|"HMAC SHA-256"| APIQuatre frontières concentrent les contrôles :
- Client vers API. Tout ce qui vient du client est non fiable. Le backend valide chaque corps par Zod, authentifie par JWT et relit le rôle en base.
- API vers stockage. PostgreSQL et Redis ne sont exposés que sur
127.0.0.1dansdocker-compose.yml. Les secrets sensibles stockés en base sont chiffrés ou hachés. - Backend vers chaîne. Le backend signe avec la clé du citoyen ou avec la clé opérateur. La chaîne fait foi : la base n'est qu'une projection reconstruite par l'indexeur.
- Contrat vers oracle. Seul le coordinateur VRF enregistré peut fournir l'aléa du tirage.
Menaces sur l'application
| STRIDE | Menace | Protection en place | Risque résiduel |
|---|---|---|---|
| Usurpation | Connexion au compte d'un autre citoyen | Mot de passe haché par bcrypt, code à 6 chiffres envoyé par email, haché en HMAC, valide 10 minutes, 5 essais par code, blocage du compte après 10 échecs par heure (Authentification) | La connexion par code email et la connexion Google restent ouvertes à tous les comptes : l'adresse email reste un facteur suffisant |
| Usurpation | Jeton Google forgé ou destiné à une autre application | Signature, émetteur, audience (GOOGLE_CLIENT_IDS) et email_verified vérifiés côté serveur |
Rattachement au compte par l'adresse email seule ; la sécurité du compte dépend de celle du compte du fournisseur |
| Usurpation | Mineur qui joue avec le compte ou la pièce d'un adulte | Pièce d'identité examinée par un agent de la Mairie (Autorisations) | Aucun contrôle de correspondance entre la personne et la pièce (pas de selfie ni de vérification du document) ; achat direct sur le contrat sans contrôle |
| Usurpation | Jeton JWT forgé ou rejoué | Algorithme HS256 imposé, iss, aud, exp et jti vérifiés, liste de révocation dans Redis (Sessions) |
Révocation perdue si Redis tombe (repli mémoire par processus) |
| Usurpation | Webhook blockchain forgé | Signature HMAC-SHA-256 sur t.corps, fenêtre de 5 minutes, rejeu refusé pendant 7 jours (Protection des API) |
Aucun |
| Altération | Injection SQL | Requêtes Prisma paramétrées, aucun SQL brut construit à partir d'une entrée | Aucun connu |
| Altération | Modification du rôle ou de champs protégés par affectation de masse | Schémas Zod explicites, champs non listés ignorés (Validation) | Aucun connu |
| Altération | Partenaire qui agit sur l'entreprise d'un autre | assertPartnerAccess compare Partner.ownerId au compte appelant (Autorisations) |
Aucun connu |
| Répudiation | Administrateur qui nie une action | Journal ADMIN_AUDIT:* pour le disjoncteur, la trésorerie, le statut des partenaires, les exports, la consultation des pièces d'identité et les décisions sur l'âge |
Le tirage et le remboursement manuels ne sont pas journalisés dans ce journal ; la chaîne garde en revanche une trace publique |
| Divulgation | Fuite de la base | Clés de portefeuille, secrets TOTP et pièces d'identité chiffrés, mots de passe, codes de secours et jetons RGPD hachés (Cryptographie) | Les emails sont en clair en base ; les pièces en attente sont lisibles avec ENCRYPTION_KEY |
| Divulgation | Consultation abusive des pièces d'identité | Routes réservées aux ADMIN avec 2FA, images effacées dès la décision, chaque consultation journalisée, Cache-Control: no-store |
Tout ADMIN peut consulter toutes les pièces en attente ; une pièce jamais traitée reste stockée sans limite de durée |
| Divulgation | Secrets dans les logs ou le dépôt | Emails masqués dans les logs, gitleaks sur tout l'historique (Chaîne logicielle) | Aucun connu |
| Divulgation | Énumération des comptes | Message d'erreur unique sur un code invalide et sur un mot de passe faux, réponse identique de /password/forgot, temps de réponse égalisé par une empreinte factice |
/signup répond 409 pour une adresse déjà inscrite ; userId dans la réponse de /login est l'identifiant réel pour un compte existant |
| Déni de service | Saturation de l'API ou force brute | Limites de débit partagées dans Redis, corps limité à 1 Mo (12 Mo et 10 envois par heure pour les pièces d'identité) | passOnStoreError: true : sans Redis, chaque instance retombe sur une limite locale |
| Déni de service | Calcul bcrypt provoqué en masse sur /signin |
30 requêtes en 10 minutes par adresse IP | bcryptjs est en JavaScript pur : une attaque distribuée consomme le processeur de l'API |
| Déni de service | Chat temps réel abusé | Messages tronqués à 280 caractères | Pas de limite de débit ni d'authentification pour chat:send |
| Élévation | Utilisateur qui appelle une route ADMIN | requireRoles('ADMIN'), rôle relu en base, 2FA exigée pour l'ADMIN |
Voir la limite sur la 2FA dans Authentification |
| Élévation | Jeton conservé après une rétrogradation | Rôle du jeton comparé au rôle en base à chaque route protégée par requireRoles |
Les routes sans requireRoles (profil, achat) acceptent le jeton jusqu'à sa révocation ou son expiration |
Menaces propres à la blockchain
Clé opérateur
Le compte opérateur (RELAYER_PRIVATE_KEY) signe la dotation de bienvenue, la pause du contrat, l'annulation d'un tour et le reversement de la commission. Son code est dans backend/src/blockchain/chain.client.ts. Le détail des rôles est sur la page Clés et garde des portefeuilles.
| Scénario | Conséquence | Protection | Limite |
|---|---|---|---|
| Fuite de la clé | L'attaquant peut mettre le contrat en pause, annuler le tour en cours et vider les ETH de l'opérateur | Valeur de développement refusée en production par backend/src/config/env.config.ts, gitleaks, révocation des rôles par l'administrateur |
En local, l'opérateur est aussi l'administrateur et le propriétaire VRF du contrat |
| Annulation abusive | Un tour en cours est annulé, tous les participants doivent réclamer leur remboursement | claimRefund rend la totalité des contributions |
Aucune perte, mais le tirage n'a pas lieu |
| Épuisement du solde | Plus de dotation de bienvenue | Dotation versée une seule fois par compte (verrou Redis et welcomeGrantTxHash) |
Réapprovisionnement manuel |
Garde des clés citoyennes
Le portefeuille est custodial : le backend crée la clé, la chiffre et signe à la place du citoyen. Quiconque obtient à la fois la base et ENCRYPTION_KEY peut signer pour tous les citoyens. Le backend en fonctionnement dispose par construction de ce pouvoir : une compromission du serveur équivaut à la compromission de tous les portefeuilles. Cette concentration est acceptable sur un réseau de test. Elle ne l'est pas avec des fonds réels.
Oracle Chainlink VRF
Le gagnant est randomWords[0] % ticketCount, calculé dans fulfillRandomWords du contrat smart-contract/contracts/BlockLucky.sol. Le hasard ne passe jamais par le backend. Trois risques demeurent :
| Risque | Situation dans le code |
|---|---|
| L'oracle ne répond pas | retryDraw (rôle opérateur) relance la demande après drawTimeout ; closeExpiredRound permet ensuite l'annulation avec remboursement |
| Nouvelle demande d'aléa | retryDraw émet une nouvelle demande et ignore la réponse de l'ancienne. Si l'opérateur et le fournisseur de l'aléa s'entendaient pour retenir une réponse défavorable, ils pourraient « relancer les dés ». La documentation de Chainlink déconseille de redemander de l'aléa pour cette raison. Le délai drawTimeout limite ce scénario sans l'exclure. |
| Remplacement du coordinateur | Le contrat hérite de VRFConsumerBaseV2Plus, donc de ConfirmedOwner. Le propriétaire peut appeler setCoordinator et désigner une autre adresse comme source d'aléa, qui choisirait alors le gagnant. Ce propriétaire est distinct des rôles AccessControl. Les scripts de déploiement proposent la propriété à VRF_OWNER_ADDRESS (par défaut l'administrateur MAIRIE_ADMIN_ADDRESS) par transferOwnership ; elle n'est effective qu'après acceptOwnership, appelé automatiquement en local par le compte n° 2 et à signer par le multisig sur Sepolia. |
Attention
Le pouvoir de setCoordinator donne à son titulaire la possibilité théorique de truquer un tirage. L'appel émet un événement CoordinatorSet public, donc détectable. Sur Sepolia, tant que le multisig n'a pas appelé acceptOwnership, le déployeur reste propriétaire : cette signature fait partie de la procédure de déploiement sur Sepolia, et l'événement doit être surveillé ensuite.
Contrat
Les protections du contrat (garde de réentrance, retraits à l'initiative du bénéficiaire, commission plafonnée à 20 %, disjoncteur) sont décrites sur la page Sécurité du contrat. Le contrat n'est pas modifiable : une faille découverte après déploiement impose une pause puis un redéploiement.
Front-running et ordre des transactions
| Scénario | Analyse |
|---|---|
| Acheter un ticket après avoir vu l'aléa | Impossible : dès requestDraw, le tour passe à l'état Drawing et _buyTickets refuse tout achat (RoundNotOpen). |
| Prévoir le gagnant avant la demande | Impossible : l'aléa VRF n'existe pas encore quand les achats sont fermés. |
| Annuler le tour en voyant la réponse VRF | Possible pour un porteur du rôle opérateur. cancelRound n'impose aucune condition d'état ni de délai. Un opérateur qui observe dans le mempool la transaction fulfillRandomWords peut en déduire le gagnant et la devancer avec cancelRound. Les participants sont remboursés intégralement, mais le tirage est censuré. |
| Déclencher le tirage au moment choisi | requestDraw est public dès que isDrawReady() est vrai. N'importe qui peut le déclencher ; cela ne donne aucun avantage, car l'aléa vient de l'oracle. |
| Devancer un retrait | claimPrize et claimRefund vérifient l'appelant (msg.sender) : une autre adresse ne peut pas retirer à sa place. |
Important
Restreindre cancelRound à l'état Open, ou exiger que drawTimeout soit écoulé quand le tour est en Drawing, supprimerait la possibilité de censure. Ce changement demande un redéploiement du contrat (voir Modifier le contrat).
Menaces hors périmètre
- Déni de service volumétrique contre l'hébergement : il relève de l'hébergeur et d'un service de protection en amont, non configuré dans le dépôt.
- Compromission du poste d'un développeur ou d'un administrateur.
- Attaque sur le réseau Ethereum lui-même ou sur l'infrastructure Chainlink.
- Ingénierie sociale des équipes de la Ville, exclue par la politique de signalement.
Mettre à jour le modèle
Le modèle est à revoir à chaque nouvelle version du contrat, à chaque nouvelle route d'API et après chaque incident traité selon le plan de réponse aux incidents. Toute nouvelle menace notable entre aussi dans le registre des risques.