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), apresdata-platformle 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/maina9aa00977, 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 un403, 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
| Sonde | Resultat |
|---|---|
| Server actions Studio sans garde | 0 sur 21 — verifiees une par une |
| Noms d'entree d'autorisation distincts | 7 |
| Idiomes de lecture de session distincts | 3 |
| Routes API sous segment dynamique | 91 |
| … signalees par un sondage automatique | 47 |
| … reellement non autorisees dans l'echantillon lu | 0 |
Route MCP (/api/mcp/[storeId]) | bearer + scopes, correcte |
/api/admin/db | withAdminRoute, 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 requireStudioActor | features/studio/prospect-actions.ts (supprime avec le plan C, ADR 0043) requireProspectPermission | |
|---|---|---|
| Resout depuis | le store (puis son org) | l'org directement |
| Repli admin plateforme | oui, nomme via: "platform-admin" | non |
| Retour | { userId, email, name, orgId, via } | { userId } |
| Tracable dans un audit row | oui — via dit comment le droit a ete obtenu | non |
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-platformdu meme jour. La version initiale de ce constat affirmait quesrc/lib/validation/« contient zero fichier ». C'est faux : le repertoire existe et contient unREADME.md. Ma sonde comptait les.tset 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. Voir2026-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.tsreimplemente la resolution (getSignedInUser+getOrgAccess+hasPermission) queguard.tsfait 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.tset 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.mdvs 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.