Sécurité
Validation des entrées, requêtes paramétrées, gestion des secrets, erreurs opaques et traitement des vulnérabilités transitives.
Le comportement sûr est celui qu'on obtient sans rien configurer. Ces règles décrivent ce que le core garantit et ce que le code applicatif doit respecter pour ne pas l'annuler.
Données entrantes
Toute donnée externe est malveillante par défaut. On valide en entrée de fonction, côté serveur uniquement. Une validation faite seulement côté client n'est pas une validation : c'est une aide à la saisie.
On ne suppose jamais que l'appelant a fait le travail. Une fonction exportée par un module peut être appelée depuis une route, une server action, une tâche cron ou un autre module.
Base de données
Uniquement des requêtes paramétrées. Jamais de SQL construit par concaténation.
core/database le garantit jusqu'aux identifiants : les noms de tables et de colonnes passent par safeIdentifier(), et ORDER BY par safeOrderBy().
Deux gardes supplémentaires évitent des dégâts silencieux :
createetupdateByIdacceptent une liste blancheallowedColumns. Toute clé absente lève. À utiliser systématiquement dès que les données viennent d'une requête HTTP, sans quoi un champ non prévu (role,is_admin) peut être écrit par mass-assignment.updateetdeleteWhereexigent au moins une condition. UnUPDATEou unDELETEsansWHERElève au lieu de balayer la table.
Secrets
Aucun token, clé d'API ou mot de passe dans le code. Tout passe par des variables d'environnement.
Pour un secret qui doit être stocké en base et relu en clair (token d'un service tiers, secret de webhook) utiliser core/crypto, qui chiffre en AES-256-GCM. Un mot de passe utilisateur, lui, est haché à sens unique par core/users.
Le .env et le .npmrc d'un projet sont suivis par git par défaut, et portent leurs secrets en clair : variables d'environnement pour le premier, token du registre privé pour le second. C'est un choix assumé, qui suppose un dépôt privé et le restreint à des lecteurs de confiance. Un dépôt qui deviendrait public expose l'ensemble, et un historique git ne se nettoie pas facilement. Les sortir de git tient à un # retiré dans le .gitignore : voir Secrets et git et lrvo-key.
Erreurs exposées
Pas de trace d'exécution, de nom de table ou de requête SQL retournés au client. On journalise côté serveur, on renvoie un message générique.
Le core applique déjà cette séparation :
DatabaseErrorn'expose qu'un message générique et le code SQLSTATE.- Les erreurs d'authentification sont journalisées et remplacées par un message générique ; seules les
UserFacingError(token expiré, par exemple) remontent verbatim. requestPasswordResetetresendVerificationActionrépondent de façon identique que le compte existe ou non, pour ne pas permettre l'énumération des comptes.
Voir Logger : ce qui est journalisé ne doit jamais contenir de mot de passe, de token de session ou de clé d'API.
Contrôle d'accès
Déclaré, jamais vérifié à la main. Une route API porte son niveau dans sa définition (auth, permission) et le routeur l'applique avant le handler. Un handler qui ré-implémenterait le contrôle créerait une seconde source de vérité qui finira par diverger. Voir API.
Le premier compte est ouvert à qui arrive en premier. Tant que lrvo_auth_users est vide, toute page /auth/* affiche le formulaire de première configuration, et la personne qui le soumet devient administrateur. Créer ce compte avant d'exposer le site. Voir Première configuration.
Un courriel non vérifié bloque la connexion. login() refuse le compte après avoir validé le mot de passe, jamais avant, pour que l'état de vérification reste invisible à qui ne détient pas les identifiants.
Côté fichiers, core/storage part du principe inverse de l'usage courant : tout est privé par défaut. Un fichier n'est servi que si son chemin correspond à un préfixe public ou à une policy explicitement déclarés.
Limitation de débit et anti-bot
La limitation de débit s'applique par IP sur les actions d'authentification, et par identité sur l'API : sur le compte quand la session est vérifiée, sur l'adresse sinon. Trois points d'attention :
- Les en-têtes de proxy ne sont lus que si
LRVO_TRUST_PROXYdéclare le nombre de proxys devant l'application. Sans cette précaution, un client forgerait unX-Forwarded-Forvariable pour contourner la limitation. X-Forwarded-Forse lit par la droite, jamais par la gauche. Un proxy ajoute l'adresse qu'il a vue à la fin de l'en-tête et laisse intact ce que le client a envoyé : la première entrée appartient au client, seules les dernières appartiennent aux proxys.- Le budget triplé de l'appelant connecté se décide sur une session résolue côté serveur. Le déduire de la présence d'un cookie le rendrait réclamable par n'importe qui.
- Les compteurs sont locaux au processus. En déploiement multi-worker, la limite effective est multipliée par le nombre d'instances.
Les actions d'authentification restent hors du multiplicateur : ce sont des parcours qui précèdent la session, et la protection contre la force brute sur login ne doit pas s'élargir parce que l'appelant détient un cookie.
Les formulaires d'authentification embarquent un honeypot et un horodatage de chargement : une soumission trop rapide, trop ancienne, ou avec le honeypot rempli est rejetée.
Sessions
Le token de session vit dans un cookie HttpOnly : il n'est jamais exposé à JavaScript. setSessionCookie et refreshSessionCookie valident le token contre la base avant d'écrire le cookie, ce qui empêche qu'un token arbitraire soit accepté.
La protection CSRF s'applique aux méthodes mutantes et s'appuie sur l'origine publique configurée. Sans NEXT_PUBLIC_URL, les requêtes mutantes sont refusées plutôt que laissées passer.
Téléversements
validateUpload applique six couches, dans l'ordre. Deux méritent d'être soulignées :
- Le SVG est explicitement interdit. Un SVG est un document exécutable, pas une image inerte.
- Les magic bytes sont vérifiés quand le buffer est fourni, avec un scan des 512 premiers octets à la recherche de HTML, SVG ou XML. C'est la défense contre les fichiers polyglots et les extensions renommées. Passer
bufferdès qu'on l'a : sans lui, ces deux couches ne s'exécutent pas.
Médias privés
Une image privée ne passe jamais par /_next/image : l'optimiseur met la réponse en cache par URL et exposerait l'image à tous les visiteurs après le premier accès. Le wrapper MediaImage route les médias publics par next/image et garde un <img> natif pour les médias privés. Voir Registre média.
Vulnérabilités transitives
On corrige à la racine, pas au symptôme. Une alerte npm audit sur un paquet enfoui se remonte jusqu'à la dépendance directe qui l'introduit : c'est elle qu'on met à jour ou qu'on remplace.
C'est ce raisonnement qui a conduit à épingler Resend en v6 : la v3 embarquait @react-email/render, qui tirait la chaîne js-beautify → minimatch → brace-expansion. Remplacer la dépendance directe a supprimé la chaîne entière plutôt que d'en override un maillon.
Ce qu'un package livre réellement
Avant de corriger, il faut savoir ce qui est livré. Trois catégories, une seule compte.
| Catégorie | Livrée au site ? | À auditer dans le dépôt |
|---|---|---|
dependencies | oui, installées avec le package | oui |
peerDependencies | non, fournies par le site | non, c'est au site de les tenir à jour |
devDependencies | non | non |
Les devDependencies ne sortent jamais du dépôt : tsup.config.js compile en bundle: false avec packages: 'external', donc aucun octet de tsup, TypeScript, esbuild ou postcss n'entre dans dist/. Voir Build et exports.
Les peerDependencies sont installées localement par npm dans le dépôt de la librairie, ce qui les fait apparaître dans un npm audit nu. C'est un artefact de l'installation de développement, pas la surface du package. Une alerte sur next, react ou une de leurs dépendances se corrige dans le site, une seule fois, pas dans chaque module.
D'où la commande d'audit du dépôt, déclarée en script audit dans chaque package.json :
npm run audit # npm audit --omit=dev --omit=peerC'est cette commande, et elle seule, qui figure dans la checklist de publication.
Le champ overrides va dans le site, pas dans le package
npm n'honore overrides que depuis le package.json racine du projet installé. Un bloc overrides déclaré dans @lrvo/core ou dans un @lrvo/module-* est purement et simplement ignoré au moment où un site les installe. Il n'agit que sur le node_modules du dépôt de la librairie.
Un overrides dans un package publié est donc au mieux inutile, au pire dangereux : il donne l'impression que la faille est traitée alors que tous les sites restent exposés. Les packages LRVO n'en déclarent aucun.
Le seul endroit où un override protège un site, c'est le package.json du site. Le bloc est maintenu dans un seul fichier, la constante SITE_OVERRIDES de @lrvo/start, qui l'écrit dans le projet à la création.
Cette constante est vide, et c'est l'état normal. Une entrée ne s'ajoute que pour une faille qu'aucune mise à jour amont ne corrige, typiquement une version épinglée exactement par une dépendance directe. Elle se retire dès que l'amont est à jour de lui-même : un override laissé en place fige une version pour rien et donne une fausse impression de correctif.
Deux limites à connaître avant d'en ajouter une :
- npm refuse un override qui porte sur un paquet déclaré dans les
dependenciesdu site, avec une erreurEOVERRIDE. Dans ce cas, c'est la dépendance directe qu'il faut corriger. - Un site déjà généré ne reçoit pas les entrées ajoutées après coup. Il faut les copier à la main dans son
package.json, puis relancernpm install.
Où corriger, en pratique
| Origine de l'alerte | Où corriger |
|---|---|
dependencies du core ou d'un module | dans le dépôt concerné, en mettant à jour la dépendance directe |
next, react ou leur arbre | dans le site, avec npm update next |
| Version épinglée par un amont, sans correctif disponible | dans SITE_OVERRIDES de @lrvo/start |
| Outillage de build | nulle part, ce n'est pas livré |
L'ordre compte : la deuxième ligne couvre la grande majorité des alertes d'un site LRVO. Next tire postcss, nanoid, sharp et tout le reste de leur arbre, si bien qu'un npm update next règle d'un coup la plupart des alertes remontées par npm audit. La cible de cette montée est le champ tested de supported.json : c'est lui que lrvo-doctor affiche dans son remède.
Un package-lock.json figé est le risque, pas l'exception. Next lui-même est régulièrement l'objet d'avis de sécurité : la ligne 16.0.0 à 16.3.2 portait deux exécutions de code à distance non authentifiées, et le sharp embarqué traînait une faille libheif sous 0.35.4, puis une faille librsvg sous 0.35.5. Un site qui n'a pas rafraîchi son verrou depuis sa création reste exposé même si son package.json autorise déjà la version corrigée.
Signaler une faille
Ne pas ouvrir d'issue publique. Envoyer un rapport à [email protected] avec la description, les étapes de reproduction, l'impact potentiel et, si possible, une suggestion de correctif.
Réponse sous 48 heures. Le correctif est développé de façon confidentielle avant toute divulgation publique, dans un délai qui ne dépasse pas 90 jours sauf entente contraire. Voir SECURITY.md.