Anti-patterns

Cette page documente les erreurs critiques rencontrées (ou anticipées) sur Le Labo du Yeti. Chaque entrée explique pourquoi le pattern est dangereux, ce qu'il casse, et la solution canonique. Considère ce document comme une checklist défensive : si une de ces situations apparaît dans une PR, refuse le merge.

Règle d'or. Le CMS doit pouvoir être déployé, restauré, mis à jour et étendu sans intervention manuelle sur la base ou les fichiers utilisateur. Tout anti-pattern listé ici viole cette règle d'une façon ou d'une autre.

1. Manipulation directe des données

1.1 Ne pas modifier content/cms.db à la main

La base SQLite est gérée exclusivement par backend/src/utils/db-store.js et les services métier (blog-service.js, cocon-service.js, pages-service.js…). Ouvrir cms.db avec DB Browser ou exécuter un UPDATE brut court-circuite :

De la même façon, les fichiers *.json.bak dans content/ sont des reliquats de la migration KV legacy. Ne les édite pas et ne les restaure pas : passe par POST /api/backup/restore qui fait un safety backup auto avant écrasement.

Conséquence réelle. Une édition manuelle de cms.db pendant que le serveur tourne en mode WAL peut produire une base corrompue ou un état où le WAL contient des écritures invisibles. Toujours closeDb() puis checkpointDb() avant tout export, et arrêter Node avant tout import.

1.2 Ne plus créer de nouveaux fichiers JSON de stockage

La règle de stockage est unique et stricte :

Type de donnéeStockageHelpers
Config rare (auth, global, ai, blog config…)Table stores (KV)readStore / writeStore / updateStore
Données chaudes (users, pages, articles, cocon, logs, tokens, AI usage)Tables SQL dédiéesHelpers spécialisés (usersInsert, logsInsert, aiUsageInsert…)
Données par extensionextension_data KV scopéeext.storage.get/set/keys/delete

Inventer un nouveau content/mon-truc.json casse les backups (le ZIP n'embarque que cms.db + uploads/ + extensions/installed/), brise la cohérence transactionnelle et empêche l'observabilité via les logs SQL.

2. Sécurité — champs sensibles, injections, SSRF

2.1 Ne jamais exposer les champs sensibles dans une réponse API

La liste des champs strictement interdits en sortie :

Le contexte injecté aux extensions (ext.cms.*) est déjà filtré. Si tu ajoutes un nouveau helper exposé aux extensions, fais un pass de relecture sur le serializer pour confirmer qu'aucun champ sensible ne fuit. Les 24 permissions documentées dans extensions.html sont là pour ça.

Exemple de fuite à proscrire. Un res.json(user) qui renvoie l'objet brut issu de usersFindById(id, { includeSecret: true }). Toujours passer par un mapper qui whitelist les champs publics.

2.2 Ne jamais utiliser shell: true avec un input utilisateur

Le CMS exécute des commandes système dans deux endroits sensibles : l'installation d'extensions (npm install) et le builder de site. Toute exécution doit suivre :

// MAUVAIS — command injection trivial
const { exec } = require('child_process');
exec(`npm install ${userPackage}`, { shell: true });

// BON — args en array, pas de shell, path absolu
const { spawn } = require('child_process');
const npmCmd = resolveNpmCommand(); // résout via sibling de node sur Plesk
spawn(npmCmd, ['install', '--no-audit', '--no-fund'], {
  cwd: extensionDir,
  shell: false
});

2.3 Ne pas extraire un ZIP sans assertZipEntriesSafe

Deux flux acceptent un ZIP de l'utilisateur : POST /api/extensions et POST /api/backup/restore. Sans guard, un attaquant peut placer une entry nommée ../../etc/passwd ou ..\\Windows\\System32\\… (zip slip). Le helper assertZipEntriesSafe(entries, targetDir) :

2.4 Ne pas fetch() une URL sans assertUrlSafe

Plusieurs flux fetchent des URLs externes : import de médias, banques d'images (Unsplash/Pexels/Pixabay), recherche web (Brave/SerpAPI/Tavily), webhooks d'extension. Toute URL fournie par un utilisateur ou par une extension doit passer par assertUrlSafe(url) qui :

3. Déploiement Plesk

3.1 Ne pas écraser les dossiers utilisateur lors d'un déploiement

Le déploiement Plesk pousse uniquement les sources backend + frontend/dist. Les chemins suivants sont la propriété du client et ne doivent jamais être déplacés, ré-initialisés ou git-pull-écrasés :

CheminContenuRisque si écrasé
content/cms.db + WALPerte totale de toutes les données
uploads/Médias uploadés (images, vidéos, PDF)Pages cassées, médiathèque vide
public/Site statique buildéSite hors-ligne jusqu'au prochain build
brouillons/Templates en coursTravail en cours perdu
extensions/installed/Extensions tiers installéesPlugins désactivés, données extension_data orphelines

Le .gitignore exclut déjà ces chemins. Vérifie qu'aucun nouveau script de release ne contourne cette exclusion par un rsync --delete mal calibré.

Anti-pattern acquis. L'auto-pull main aveugle sur Plesk. Le workflow correct est : tag Git → build CI → push branche release → déploiement Plesk qui ne touche QUE les sources.

3.2 Ne pas appeler npm install sans path absolu sur Plesk

Plesk fournit Node via un installeur qui ne place pas toujours npm dans le PATH du process Node de l'application. Le helper resolveNpmCommand() trouve npm comme sibling du binaire node en cours d'exécution :

// Sur Plesk : /opt/plesk/node/22/bin/node
// → npm est à : /opt/plesk/node/22/bin/npm
const npmPath = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');

Référence : commit bbe809e Fix(extensions): résoudre npm via sibling de node (Plesk-safe).

3.3 Ne pas oublier le WAL checkpoint avant copie de cms.db

SQLite en mode WAL maintient les écritures récentes dans cms.db-wal. Copier cms.db seule donne un snapshot incohérent (les dernières écritures sont absentes). Toujours :

await checkpointDb();        // PRAGMA wal_checkpoint(TRUNCATE)
// puis copier cms.db
// (optionnel : closeDb() si on doit déplacer/remplacer le fichier)

Le helper checkpointDb() est appelé automatiquement par GET /api/backup/export ; n'écris pas un script de backup parallèle qui l'oublie.

4. CSP, sécurité front et confusion admin/public

4.1 La CSP est sur /admin uniquement

Helmet et la Content Security Policy stricte (script-src 'self', pas d'inline) ne protègent que l'interface d'administration React. Le site public servi depuis public/ n'a pas cette CSP, parce qu'il doit pouvoir embarquer :

Conséquence : ne pas compter sur la CSP pour bloquer un script inline malicieux sur le site public. Le seul rempart est l'assainissement à l'écriture dans les pages/articles/cocon, et la signature des extensions au moment de l'install.

4.2 Le frontend cache l'UI, le backend fait foi

Le frontend lit GET /api/capabilities pour filtrer la sidebar et masquer les boutons interdits. C'est de l'UX, pas de la sécurité. Toute action côté serveur doit :

  1. Passer requireAuth
  2. Passer requireRole(...) si applicable
  3. Passer requireCapability(featureId) si la feature est dans CORE_FEATURES

Si tu ajoutes un endpoint sans requireCapability alors que la feature existe, un editor peut potentiellement appeler la route directement même si le bouton est caché dans la sidebar.

Cas particulier owner. Le rôle owner bypasse requireCapability() dès la première ligne du middleware (if (role === 'owner') return next()). Ne tente pas de "désactiver une feature pour owner" via la matrice : c'est volontairement impossible.

5. RBAC — changements de rôle et règles métier

5.1 Ne jamais changer un rôle hors de updateAdmin(id, patch, caller)

La règle "seul owner peut changer un rôle" est implémentée dans le service auth-service.js, pas dans la route. Si une nouvelle route de bulk-edit appelle directement usersUpdate(id, { role }), la règle saute. Toutes les mutations de rôle DOIVENT passer par :

await updateAdmin(targetId, { role: 'admin' }, callerUser);
// Le service vérifie :
// - callerUser.role === 'owner'
// - on ne dégrade pas le dernier owner
// - on ne crée pas un orphelin de propriété

5.2 Ne pas implémenter une règle de capability uniquement côté front

Exemple typique : "l'editor ne peut voir que son propre compte dans Réglages → Comptes". La règle doit exister dans GET /api/auth/admins côté backend (filtre par req.user.id si rôle editor) et côté front (masquer les autres lignes). Front-only = trivialement contournable via cURL.

6. Cohérence UI — settings primitives

6.1 Ne pas écrire de HTML brut pour des composants Settings

Tous les écrans /settings/* doivent utiliser frontend/src/components/settings-primitives.jsx :

Court-circuiter ces primitives produit des écrans qui désynchronisent visuellement à la moindre mise à jour des tokens CSS (frontend/src/styles/tokens.css), et casse le mode sombre.

7. Pièges IA

7.1 Ne pas tronquer les générations IA avec un max_tokens trop bas

Anthropic Claude requiert au minimum 8192 tokens de sortie pour générer un article de blog complet ou une page cocon riche. Un max_tokens: 4096 produit des HTML coupés en plein <section>, ce qui :

Configure systématiquement max_tokens: 8192 minimum dans ai-service.js pour les flux articles/cocon. Référence en cas de doute : la table ai_usage permet d'auditer output_tokens par kind et action via aiUsageRecent().

7.2 Ne pas oublier aiGenLimiter sur un nouveau flux IA

Tous les endpoints qui déclenchent un appel Anthropic (génération cocon, génération article, regénération image) sont rate-limités à 10/min par IP via aiGenLimiter. Un nouveau flux qui l'oublie peut être abusé pour épuiser le budget API du client.

8. Git — secrets et mémoire

8.1 Ne jamais committer .env, MEMORY.md, cms.db

Le .gitignore protège déjà ces fichiers, mais reste vigilant lors d'opérations git add -A ou git add . :

Préfère git add <fichiers spécifiques> à git add .. Si un secret est committé par erreur, considère-le compromis : rotation immédiate de la clé concernée, jamais un simple git reset.

9. Checklist de revue de PR

Avant de merger, balayer cette liste :

  1. Aucun nouveau fichier *.json dans content/ ou ailleurs comme stockage métier.
  2. Aucune route qui renvoie password_hash, token_hash, api_key, smtp.pass, JWT_SECRET.
  3. Tout exec() / spawn() sur input utilisateur utilise shell: false et args en array.
  4. Tout unzip passe par assertZipEntriesSafe.
  5. Tout fetch() sur URL externe utilisateur passe par assertUrlSafe.
  6. Toute mutation de users.role passe par updateAdmin(id, patch, caller).
  7. Tout endpoint sensible a requireAuth + requireRole + requireCapability.
  8. Toute génération IA d'article ou cocon a max_tokens ≥ 8192 et le aiGenLimiter.
  9. Aucun composant /settings/* n'utilise de HTML brut hors settings-primitives.jsx.
  10. Aucun script de release ne touche content/, uploads/, brouillons/, public/, extensions/installed/.
En cas de doute. Croise toujours avec security.html pour le détail des protections, auth.html pour le RBAC, database.html pour les helpers DB, et extensions.html pour les permissions exposées aux plugins.