BlockLucky Documentation officielle GitHub

Validation des entrées

Toute donnée qui entre dans le backend par un corps de requête passe par un schéma Zod avant d'atteindre un contrôleur. Les sorties sensibles (emails HTML, exports CSV, journaux) sont neutralisées. Cette page décrit le mécanisme, les bornes appliquées et les zones qui échappent encore à la validation.

Le middleware validate

backend/src/middlewares/validate.middleware.ts reçoit un schéma de forme { body, query, params } et appelle schema.parseAsync.

Cas Réponse
Données valides req.body est remplacé par la valeur analysée : champs inconnus retirés, valeurs converties (z.coerce), valeurs par défaut appliquées
Erreur Zod 400 Erreur de validation des données, avec la liste { path, message }
Autre erreur Transmise au gestionnaire d'erreurs
JSON
{
  "success": false,
  "message": "Erreur de validation des données",
  "errors": [
    { "path": "body.walletAddress", "message": "Adresse de portefeuille invalide" }
  ]
}

Les objets Zod sont non stricts par défaut : un champ non déclaré n'est pas rejeté, il est retiré. C'est ce qui protège contre l'affectation de masse. Un client qui ajoute "role": "ADMIN" au corps de /api/auth/login n'a aucun effet, puisque role ne figure pas dans socialLoginSchema.

Seul req.body est réécrit. Les paramètres d'URL et de requête (req.params, req.query) peuvent être vérifiés par un schéma, mais le contrôleur lit toujours la valeur brute. Les schémas des routes de vérification d'identité et de gestion des comptes de l'administration sont les seuls à déclarer query et params ; ailleurs, ces valeurs sont converties à la main dans les contrôleurs (voir Limites connues).

Schémas et bornes

Chaque module a son fichier *.validators.ts. Le tableau résume les règles de sécurité qu'ils imposent.

Schéma Fichier Règles notables
socialLoginSchema backend/src/api/auth/auth.validators.ts Adresse ^0x[0-9a-fA-F]{40}$ mise en minuscules ; email de 254 caractères au plus ; isAdultConfirmed doit valoir true ; acceptTerms facultatif, mais true s'il est présent ; provider (GOOGLE, EMAIL, WALLET) et ageGroup en énumération ; code de parrainage 64 et UTM 100 caractères au plus
signupSchema, resetPasswordSchema idem Mot de passe de 10 à 128 caractères avec au moins une lettre et un chiffre (passwordPolicy) ; email de 254 caractères au plus, mis en minuscules ; acceptTerms et isAdultConfirmed doivent valoir true à l'inscription ; code de réinitialisation de 6 chiffres
signinSchema idem Mot de passe de 1 à 128 caractères, refusé au-delà avec le message générique « Email ou mot de passe incorrect. »
googleOAuthSchema idem Jeton de 20 à 8 192 caractères
verify2FASchema, disable2FASchema idem Code de 6 à 16 caractères [0-9A-Za-z-], userId en UUID ; loginTicket facultatif de 32 à 128 caractères [A-Za-z0-9_-]
buyTicketSchema backend/src/api/lottery/lottery.validators.ts Quantité égale à 1 : « Un seul ticket par participant et par grand tirage. »
giftTicketSchema idem Email du bénéficiaire, message personnel de 250 caractères au plus
claimPrizeSchema idem Numéro de tour de 1 à 1 000 000
claimRewardSchema, usePromoCodeSchema backend/src/api/marketplace/marketplace.validators.ts UUID ; code promo de 4 à 64 caractères
scratchTicketSchema idem Offre violet ou lucky
createPartnerSchema backend/src/api/partners/partners.validators.ts Nom de 2 à 100 caractères ; logo en URL HTTPS de 500 caractères au plus
createRewardOfferSchema idem Coût de 10 à 100 000 points, stock de 1 à 100 000, date ISO 8601 avec fuseau
uploadCodesBatchSchema idem Au plus 5 000 codes de 3 à 64 caractères, ou un CSV de 200 000 caractères au plus
submitIdentitySchema backend/src/api/user/user.validators.ts Recto et verso obligatoires, chaînes de 16 caractères au moins et de la taille d'une image de 4 Mo encodée en base64 au plus
updatePreferencesSchema, completeSocialQuestSchema idem Énumérations fermées
confirmGdprDeletionSchema idem Jeton de 10 à 128 caractères
trackEventSchema backend/src/api/analytics/analytics.validators.ts Type ^[A-Za-z0-9_:.-]{2,64}$, préfixes réservés refusés, au plus 20 métadonnées de types simples, chaînes de 200 caractères au plus
circuitBreakerSchema, treasuryWithdrawalSchema backend/src/api/admin/admin.validators.ts Motif d'au moins 5 caractères ; adresse Ethereum ; montant positif plafonné à 1 000 ETH ; note de 500 caractères au plus
listStaffAccountsSchema, createStaffAccountSchema, updateStaffRoleSchema idem Rôle ADMIN ou PARTNER en filtre et à la création, USER, PARTNER ou ADMIN au changement ; recherche de 120 caractères au plus ; limit de 1 à 100 ; email valide mis en minuscules ; prénom et nom de 80 caractères au plus ; partnerId en UUID, refusé si le rôle n'est pas PARTNER ; identifiant de compte en UUID
listIdentityVerificationsSchema, identityDecisionSchema idem Statut en énumération ; identifiant de demande en UUID ; décision booléenne ; motif de refus de 3 à 500 caractères obligatoire quand approved vaut false
blockchainWebhookSchema backend/src/api/webhooks/webhooks.validators.ts Union discriminée sur eventType ; hash ^0x[0-9a-fA-F]{64}$ ; adresses vérifiées ; prix plafonné à 10 ETH

Les variables d'environnement sont elles aussi validées par Zod au démarrage, dans backend/src/config/env.config.ts : le processus s'arrête si une valeur est invalide (voir Cryptographie et secrets).

Pièces d'identité

Le schéma de POST /api/user/identity ne borne que la longueur. Le contenu est contrôlé ensuite par decodeIdentityImage de backend/src/utils/identity-image.util.ts, pour le recto comme pour le verso :

Contrôle Refus
Préfixe data:image/jpeg;base64, ou data:image/png;base64, facultatif, puis base64 strict (longueur multiple de 4, alphabet standard) 400
Taille décodée de 4 Mo au plus 413 « Image recto trop volumineuse : 4 Mo au plus. »
Taille décodée d'au moins 1 Ko 400
Signature binaire : FF D8 FF pour JPEG, 89 50 4E 47 0D 0A 1A 0A pour PNG 400, quel que soit le type déclaré dans le préfixe

Le type déclaré par le client n'est jamais cru : seuls les premiers octets décident. Les images sont chiffrées dès la validation (voir Cryptographie et secrets) et ne sont jamais écrites sur disque ni dans les journaux. Elles ne sont pas réencodées : les métadonnées EXIF d'une photo envoyée par un autre client que le mobile, qui demande exif: false, sont conservées jusqu'à l'effacement. Le test refuse une image trop lourde, un format inconnu ou une face manquante de backend/__tests__/api/identity.test.ts couvre ces règles.

Le cas des événements analytiques

POST /api/analytics/track est ouvert aux anonymes. Sans contrôle, un client pourrait insérer de faux événements ADMIN_AUDIT:..., qui apparaîtraient dans le journal d'administration puisque celui-ci est stocké dans la même table. Le schéma refuse donc tout type commençant, sans tenir compte de la casse, par ADMIN_AUDIT, USER_SIGNUP ou USER_LOGIN. Le test refuse les événements analytiques qui imitent le journal d'administration le vérifie.

Limites de taille

Limite Valeur Où
Corps JSON 1 Mo globalJsonParser de backend/src/middlewares/body-parser.middleware.ts, monté dans backend/src/app.ts
Corps JSON de POST /api/user/identity 12 Mo, analysé seulement après authenticate et la limite de 10 envois par heure identityUploadJsonParser
Corps URL-encodé 1 Mo express.urlencoded
Corps trop gros 413 Corps de requête trop volumineux backend/src/middlewares/error.middleware.ts
JSON mal formé 400 Corps de requête JSON invalide idem
Pagination 1 à 100 éléments par page getPaginationParams de backend/src/utils/pagination.util.ts
Message de chat 280 caractères backend/src/socket/events/chat.events.ts
Identifiant de requête entrant ^[A-Za-z0-9._-]{8,128}$, sinon remplacé backend/src/middlewares/request-context.middleware.ts
Corps sur le site de l'Office de Tourisme 16 Ko, méthodes GET et HEAD seules frontend-office-tourisme/docker/nginx.conf

Injections

Type Situation
SQL Toutes les requêtes passent par Prisma, qui paramètre les valeurs. La seule requête brute, SELECT 1 du contrôle de santé, utilise la forme littérale balisée prisma.$queryRaw`...` sans aucune interpolation.
NoSQL et Redis Les clés Redis sont construites à partir d'identifiants internes (UUID, jti) ou de valeurs validées ; aucune commande n'est construite à partir d'une chaîne fournie par le client.
Commandes système Le backend n'exécute aucune commande système.
Chemins de fichier Les modèles d'email sont choisis dans une liste fermée (EmailTemplateName). Le fichier POAP est filtré par l'expression ^(\d{1,9})\.json$ ou .svg.
Requêtes sortantes Les seules URL appelées par le backend sont configurées par l'environnement (RPC, SMTP, S3). Le logo d'un partenaire est une URL HTTPS stockée, jamais téléchargée par le serveur.

Échappement des sorties

Emails

backend/src/services/email.service.ts remplace chaque {{VARIABLE}} des modèles HTML par sa valeur échappée :

TypeScript
const escapeHtml = (value: string): string =>
  value.replace(/[&<>"']/g, (char) =>
    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char] ?? char
  );

Un message personnel de cadeau contenant <script> arrive donc sous forme de texte dans la boîte du destinataire. Les modèles sont dans backend/src/templates/emails.

Exports CSV

L'export comptable (GET /api/admin/reports/export) passe chaque cellule par neutralizeSpreadsheetCell de backend/src/utils/crypto.util.ts :

  • la cellule est entourée de guillemets doubles et ses guillemets internes sont doublés ;
  • si elle commence par =, +, -, @, une tabulation ou un retour chariot, une apostrophe est ajoutée devant pour qu'un tableur ne l'interprète pas comme une formule.

Journaux

Les adresses email sont masquées par maskEmail (j***@domaine) dans les journaux d'envoi. Aucun code, jeton ou secret n'est journalisé. Les erreurs non gérées renvoient en production le message générique Erreur interne du serveur et l'identifiant requestId, sans trace technique.

Frontends

Le web (React) et le mobile (React Native) affichent les données comme du texte. Le code ne contient aucun appel à dangerouslySetInnerHTML sur une donnée reçue de l'API. Le site de l'Office de Tourisme est traité dans Navigateur et mobile.

Limites connues

Limite Détail
Paramètres de requête non bornés GET /api/lottery/leaderboard?limit= est public et transmet Number(req.query.limit) (20 par défaut) à Prisma sans plafond. Une valeur très grande déclenche une requête lourde. GET /api/admin/audit-logs?limit= a le même défaut, réservé aux administrateurs.
Événements Socket.IO chat:send et chat:react ne passent pas par Zod. chat:react appelle emoji.slice(0, 8) sans vérifier le type : une valeur non textuelle lève une exception dans le gestionnaire. Le champ emoji de chat:send n'est pas borné.
Motif du disjoncteur reason a une longueur minimale mais pas de maximum (le corps reste limité à 1 Mo).
Modèle d'email de secours Si un fichier de modèle manque, loadTemplate renvoie JSON.stringify(variables) dans un paragraphe HTML sans échappement. Ce cas ne se produit pas avec les modèles livrés.
En-tête Host Les métadonnées POAP construisent leur URL à partir de req.get('host') et sont mises en cache immutable. Derrière un proxy qui ne réécrit pas l'en-tête, un client peut faire apparaître un autre domaine dans la réponse.

Ces points figurent dans la synthèse des audits. Pour ajouter un schéma à une nouvelle route, suivre le guide Ajouter un endpoint.