Audits · août 2026Audit — pilier security-identity

Audit — pilier security-identity

Deuxieme tour de la rotation (docs/team/roster.md, @codebase-auditor), apres data-platform le meme jour. Lecture seule sur le code. Ce document et trois items de backlog sont la sortie complete. Perimetre…

Deuxieme tour de la rotation (docs/team/roster.md, @codebase-auditor), apres data-platform le meme jour.

Lecture seule sur le code. Ce document et trois items de backlog sont la sortie complete.

Perimetre : src/modules/auth/**, src/lib/security/**, src/lib/validation/**, src/app/api/auth|keys|security/**, src/app/(minimal)/auth|invite/**, scripts/audit-memory-scope.sh, docs/ops/secret-rotation.md.

Etat du depot : origin/main a 9aa00977, 25 aout 2026.

Une premisse a corriger d'abord

Cet audit avait ete motive, la veille au soir, par : « le chantier Studio vient d'ajouter 11 permissions studio.* et 4 roles preconfigures, et personne n'a jamais audite si ces gardes tiennent ».

La moitie de cette phrase etait fausse. src/lib/security/studio-guard.test.ts et la section studio de permissions.test.ts couvrent deja, et serieusement :

  • le refus d'un appelant non authentifie avant meme la lecture de membership ;
  • le refus d'une org dont l'appelant n'est pas membre ;
  • le preset resolu sur la membership, pas sur la personne ;
  • un admin d'org sans preset refuse : « admin » n'est pas un role Studio ;
  • le proprietaire qui passe sans preset ;
  • et un 404 — jamais un 403, jamais une redirection : identique pour une org etrangere et pour une permission manquante, pour qu'un sondage ne puisse pas distinguer les deux.

Ce dernier point n'est pas un test de routine : c'est une decision de conception contre l'enumeration, ecrite et verrouillee. Le namespace studio.* est en meilleur etat que la phrase qui a motive cet audit.

Ce qui manquait n'etait pas des tests. C'etait un regard sur ce que les tests ne peuvent pas voir : est-ce que toutes les surfaces passent bien par ces gardes, et peut-on le verifier autrement qu'a la main.

Ce qui a ete verifie

SondeResultat
Server actions Studio sans garde0 sur 21 — verifiees une par une
Noms d'entree d'autorisation distincts7
Idiomes de lecture de session distincts3
Routes API sous segment dynamique91
… signalees par un sondage automatique47
… reellement non autorisees dans l'echantillon lu0
Route MCP (/api/mcp/[storeId])bearer + scopes, correcte
/api/admin/dbwithAdminRoute, correcte
src/lib/validation/** (dans la carte d'ownership)n'existe pas

Constat 1 — deux implementations de l'autorisation Studio, et un commentaire qui affirme qu'il n'y en a qu'une

Severite : moyenne. → backlog/security-identity/0051

Deux fonctions repondent a « qui peut agir sur les donnees Studio de cette organisation », et elles ne repondent pas pareil.

features/studio/guard.ts requireStudioActorfeatures/studio/prospect-actions.ts (supprime avec le plan C, ADR 0043) requireProspectPermission
Resout depuisle store (puis son org)l'org directement
Repli admin plateformeoui, nomme via: "platform-admin"non
Retour{ userId, email, name, orgId, via }{ userId }
Tracable dans un audit rowoui — via dit comment le droit a ete obtenunon

guard.ts documente son repli : un operateur a une raison legitime de debloquer la file d'une marque, et il passe alors par une porte nommee et enregistree plutot que par la seule porte existante (ADR 0004).

prospect-actions.ts n'a pas ce repli. Consequence concrete : le meme operateur plateforme, dans la meme urgence, peut debloquer une QC, un drop, un concept ou un gate, mais pas un pipeline de prospects.

Ce qui rend le constat solide plutot qu'esthetique, c'est l'en-tete du fichier :

« Authorization goes through the same door as the rest of the Studio. »

Ce n'est pas le cas. C'est une seconde porte, avec une autre regle, dans un fichier sensible, decrite comme etant la premiere.

L'auditeur ne tranche pas laquelle est la bonne. Les deux lectures se defendent : un pipeline commercial est peut-etre precisement ce qu'un admin plateforme ne doit pas lire ; ou le repli est deliberement general et c'est un oubli. Le constat est qu'elles divergent et que la doc dit l'inverse.


Constat 2 — la couverture d'autorisation n'est verifiable qu'a la main

Severite : moyenne. → backlog/security-identity/0052

Sept noms differents ouvrent la meme porte :

requireStudioActor · requireStudioActorForAsset · requireStudioActorForDrop · actorForConcept · requireProspectPermission · loadProspectFor · requireOrgMembership (features/ai/branches/shared.ts:36)

Et trois facons de lire la session : getSession, getSignedInUser, getServerSession(authOptions).

La preuve est cet audit lui-meme. Un sondage automatique cherchant un helper d'autorisation connu dans les 91 routes API sous segment dynamique en a signale 47. Chaque route echantillonnee s'est revelee correctement autorisee : parfois un etage plus bas, comme /api/branches/[id]/publish qui n'a aucun controle et appelle publishBranch, laquelle appelle requireOrgMembership.

Trois fois de suite, sur les actions Studio, mes propres sondes ont crie au loup pour la meme raison : le helper s'appelait autrement.

Le probleme n'est pas le faux positif. C'est ce qu'il implique :

  • aucun outil ne peut affirmer que toutes les routes sont gardees : ni une garde CI, ni un relecteur presse, ni un auditeur ;
  • une route qui oublierait reellement le controle serait indistinguable des 47 qui ne l'oublient pas ;
  • rien n'empeche une huitieme porte d'apparaitre demain.

Ce qui est verifie ici, c'est que l'etat actuel est bon, et invérifiable. Les deux a la fois.


Constat 3 — le pilier declare un perimetre qui ne contient pas ce qu'il annonce

Corrige le 25 aout 2026, par l'audit ai-platform du meme jour. La version initiale de ce constat affirmait que src/lib/validation/ « contient zero fichier ». C'est faux : le repertoire existe et contient un README.md. Ma sonde comptait les .ts et j'ai lu son resultat comme « repertoire absent ». Le constat reel est plus interessant que celui que j'avais ecrit, et il est ci-dessous. Voir 2026-08-25-ai-platform.md, constat 1.

Severite : faible. → backlog/security-identity/0053

.claude/fleet/ownership.json et docs/team/roster.md attribuent src/lib/validation/** a security-identity. Le repertoire contient un seul fichier : un README.md titre # packages/core/src/validation, l'arborescence monorepo abandonnee en avril 2026, qui documente feedback.ts, index.ts et validator.ts. Aucun des trois n'existe.

Consequences : pnpm fleet:scope n'y routera jamais de code, le roster promet un perimetre qui n'a pas de code, et un contributeur, ou un agent, qui ouvre ce README lit qu'un systeme de validation inter-agents vit la. La validation reelle vit dans les schemas zod disperses au point d'usage.

Ce n'est pas un cas isole : l'audit ai-platform en a trouve treize du meme type, dont sept documentant des fichiers absents. La correction de fond appartient a l'item ai-platform/0056 ; celui-ci ne porte que l'entree d'ownership.json.


Releve, sans item

  • prospect-actions.ts reimplemente la resolution (getSignedInUser + getOrgAccess + hasPermission) que guard.ts fait deja. C'est la cause mecanique du constat 1 ; le corriger est le contenu de l'item 0051, pas un constat separe.

Ce que cet audit n'a pas couvert

  • le flux magic link / OTP (src/app/(minimal)/auth, 652 lignes) : lu en diagonale, aucune sonde sur l'expiration, le rejeu, ou le rate-limit ;
  • crypto.ts et la derivation de cle : non exerces ; l'incident de derivation de cle de juin (item 0004) n'a pas ete rejoue ;
  • docs/ops/secret-rotation.md vs la realite : non confronte ;
  • les 44 routes API restantes de l'echantillon, lues seulement par leur en-tete.

Un auditeur qui ne borne pas sa portee laisse croire qu'il a tout vu.


Prochain pilier de la rotation : billing, et le motif est concret, pas rituel : un item 0007 y dort depuis le 19 aout, le catalogue de plans a une source de verite unique (PLAN_PRICING) qui merite la meme sonde de drift que celle passee sur le schema, et l'idempotence des webhooks Stripe est le genre d'invariant dont l'absence ne se voit qu'une fois.