Date : 15/02/2026 Status : IMPLEMENTEE Auditeur confrontation : Claude Opus 4.6 Methode : Lecture integrale des 3 rapports d'audit independants, puis verification dans le code source des points de desaccord ou d'ambiguite.
| Metrique | Nombre |
|---|---|
| Findings CONFIRMES (2-3 passes concordantes) | 14 |
| Findings UNIQUES (1 seule passe) | 8 |
| Faux positifs elimines | 0 |
| Contradictions resolues | 4 |
Repartition des findings confirmes par severite :
| Severite definitive | Nombre |
|---|---|
| BUG CERTAIN | 7 |
| BUG PROBABLE | 4 |
| RISQUE THEORIQUE | 3 |
verifyJWT() dans src/middleware/jwt.ts (lignes 29-52) decode le payload base64 du JWT et verifie uniquement l'expiration (exp), mais ne verifie JAMAIS la signature HMAC. Le commentaire TODO dans le code confirme le caractere intentionnel de cette omission. Toute valeur sub (user_id) est acceptee sans aucune preuve cryptographique. La variable JWT_SECRET / SUPABASE_JWT_SECRET est presente dans l'environnement de production mais n'est jamais utilisee par cette fonction.sub et se faire passer pour n'importe quel utilisateur. La fonction est utilisee par getAuthFromRequest() et getUserFromRequest(), ce qui propage le defaut a la quasi-totalite des endpoints.private: true + sidecar WireGuard. Seuls /api/oauth/callback et /health sont publics.getAuthFromRequest(). La liste unifiee (union des 3 passes) :| Endpoint | Confirme par | Risque |
|---|---|---|
DELETE /api/connectors/:id |
P1 + P2 + P3 | Suppression arbitraire |
POST /api/connectors/:id/token |
P1 + P2 + P3 | Le plus critique : injection de token systeme |
POST /api/connectors/:id/provision-type |
P1 + P2 + P3 | Modification config |
GET /api/connectors/:id/schema |
P1 + P3 | Information disclosure |
GET /api/connectors/:id/openapi |
P1 + P3 | Information disclosure |
GET /api/audit/logs/:id |
P1 + P2 + P3 | Lecture logs sans controle |
GET /api/connectors/:id/logs |
P1 + P2 + P3 | Lecture logs sans controle |
POST /api/connectors |
P2 | Creation sans auth (mais utilise getUserFromRequest sans throw) |
PUT /api/endpoints/:id |
P2 + P3 | Modification endpoint (utilise getUserFromRequest sans throw) |
DELETE /api/endpoints/:id |
P2 + P3 | Suppression endpoint (utilise getUserFromRequest sans throw) |
GET /api/audit/logs |
P2 | Listing tous les logs |
GET /api/connectors/:id/endpoints |
P3 | Lecture endpoints |
Precision importante : Certains endpoints (comme POST /api/connectors, PUT /api/endpoints/:id, DELETE /api/endpoints/:id) appellent getUserFromRequest() mais ne font PAS de throw ou return 401 si le resultat est null. Ils continuent l'execution avec userId = null. Ce sont donc des endpoints "partiellement" sans auth -- ils lisent l'identite mais ne l'exigent pas.
/api/fetch (le proxy principal vers tous les services connectes) n'exige pas d'authentification. getAuthFromRequest() est appele mais si le resultat est null, l'execution continue avec userId = null. Le code retombe alors sur les tokens systeme des connecteurs (via auth.getAuthHeader()). Cela signifie qu'un appel anonyme peut utiliser les credentials systemiques (API keys GitLab, Supabase, etc.) de n'importe quel connecteur.private: true bloque l'acces externe. /api/fetch n'est pas dans public_paths.wireguard.ts utilise la syntaxe supabase.from('table').select().eq().single() qui est l'API de @supabase/supabase-js, alors que le projet utilise un wrapper custom (src/db/supabase.ts) qui n'expose que supabase.select(), supabase.insert(), etc. Toutes les fonctions de wireguard.ts crashent a l'execution avec TypeError: supabase.from is not a function.vpn-profiles.ts (qui utilise la bonne API) semble etre le remplacement fonctionnel. Les endpoints VPN dans index.ts appellent vpn-profiles.ts.transform dans actions.ts (ligne ~853) executent du code JavaScript fourni par l'utilisateur via new Function(). Le code tourne dans le processus principal Bun sans sandbox. L'objet ctx expose executeAction() et proxyFetch(). Le code a acces a process.env (secrets), au filesystem, et a require/import pour executer des commandes systeme.new Function().echo '...' sans echapper les single quotes (ssh.ts, ligne 997). Si la cle contient une apostrophe, la commande shell est vulnerable a l'injection.generateKeyPairSync) et ne contient pas de single quotes (format base64 + prefixe). Le risque est reel uniquement si la cle provient d'une entree utilisateur externe. Le risque est donc faible mais l'echappement est trivial a ajouter.Confirme par : Pass 1 (#7) + Pass 2 (F-10)
Severite definitive : RISQUE THEORIQUE
Description consolidee : Le parametre state OAuth encode credential_id, user_id et un nonce (randomUUID) en base64url, mais le nonce n'est jamais stocke cote serveur ni verifie au callback. Un attaquant connaissant credential_id et user_id pourrait forger un state valide.
Verification dans le code : La fonction handleCallback() (oauth.ts, ligne 121) decode le state, extrait credential_id, puis verifie credential.user_id !== stateData.user_id. C'est une verification basique mais le credential_id est un UUID v4, ce qui rend le brute-force impraticable. Le callback /api/oauth/callback est public.
Nuance entre passes : Pass 1 mentionne randomBytes(16).toString('hex') comme nonce, alors que le code reel utilise crypto.randomUUID(). Les deux passes s'accordent sur l'absence de verification cote serveur.
Impact : Faible en pratique. L'exploitation necessite de connaitre le UUID du credential de la victime.
Priorite de correction : BASSE
ssh.ts (ligne ~943) retourne toujours true dans le hostVerifier. Aucune verification de cle d'hote SSH n'est effectuee. Le commentaire "Accept all for now" confirme le caractere temporaire.ai.ts (192.168.1.12:5501), notify.ts (192.168.1.12:5300), supabase.ts (192.168.1.12:8000). En production, les variables d'environnement sont injectees via conf.prod.gouroubleu.yml et les fallbacks ne sont pas utilises. Cependant, les regles CLAUDE.md interdisent explicitement les IPs dans le code applicatif.NODE_TLS_REJECT_UNAUTHORIZED=0 dans conf.prod.gouroubleu.yml desactive la verification TLS pour TOUTES les connexions HTTPS sortantes. Le code dans fetch.ts (ligne 171) gere deja les certificats auto-signes des services internes de maniere ciblee (IPs 192.168.x et 10.x). La variable globale est donc redondante et dangereuse pour les connexions vers des APIs externes (GitLab, GitHub, Google, etc.).encrypt() dans ssh.ts, wireguard.ts, et vpn-profiles.ts utilisent scryptSync(ENCRYPTION_KEY, 'salt', 32) avec un sel fixe. Cela annule l'interet du salt : la cle derivee est identique pour toutes les operations. L'IV est aleatoire (correct), donc le chiffrement reste different pour chaque valeur. Le probleme est que si la cle maitre fuite, toutes les valeurs sont immediatement dechiffrables (pas de sel unique pour ralentir l'attaque). En production, la cle est injectee via un secret Docker.Confirme par : Pass 2 (F-04) + Pass 3 (F-13)
Severite definitive : BUG PROBABLE
Description consolidee : La fonction synchrone getUserFromRequest() (jwt.ts, lignes 130-161) ne peut pas valider les API keys (operation asynchrone necessitant une requete DB). Les endpoints utilisant cette fonction rejettent silencieusement les utilisateurs authentifies par API key. requireAuth() (ligne 166) utilise cette version synchrone, ce qui signifie que les endpoints proteges par requireAuth() ne supportent PAS les API keys.
Endpoints affectes (utilisant getUserFromRequest au lieu de getAuthFromRequest) :
POST /api/connectors (ligne 1052)POST /api/connectors/:id/rediscover (ligne 1127)PUT /api/endpoints/:id (ligne 1387)DELETE /api/endpoints/:id (ligne 1440)Impact : Les utilisateurs avec API key ne peuvent pas utiliser ces endpoints. Le fonctionnement JWT n'est pas affecte.
Priorite de correction : MOYENNE
/api/webhooks/:routineId (ligne 5324) avec un token optionnel et sans anti-replay. Pass 3 identifie le webhook AI (ligne 4936) avec un secret optionnel (AI_WEBHOOK_SECRET). Les deux passes convergent sur le meme pattern : la verification du secret/token est conditionnelle -- si le secret n'est pas configure, le webhook est ouvert.private: true)Confirme par : Pass 3 (F-09) + verification dans le code source
Severite definitive : BUG CERTAIN (reclasse apres verification dans le code)
Description consolidee : L'interface AuditLogInput (audit.ts, lignes 64-75) definit des proprietes en camelCase (userId, connectorId, resourceId). Cependant, plusieurs appels dans index.ts utilisent des proprietes snake_case (user_id, connector_id, resource_id). Verifie dans le code source : lignes 1352-1359, 1405-1412, 1448-1455 de index.ts.
De plus, certains de ces appels passent un deuxieme argument request alors que audit.log() n'accepte qu'un seul argument AuditLogInput.
En TypeScript strict, ces appels devraient generer des erreurs de compilation. Cependant, comme le premier argument est un objet literal, les proprietes snake_case sont ignorees (proprietes supplementaires dans un objet), et les proprietes camelCase attendues (userId, connectorId, resourceId) sont undefined. Le deuxieme argument request est simplement ignore par la fonction.
Resultat a l'execution : audit.log() insere un log avec user_id: null, connector_id: null, resource_id: null dans la DB. Les proprietes action, resource et details (quand presentes) sont correctement passees. Les logs d'audit sont incomplets.
Impact : Les logs d'audit pour les operations sur endpoints (create, update, delete) n'ont pas de userId, connectorId, ni resourceId. Tracabilite degradee.
Priorite de correction : HAUTE (les logs d'audit sont un outil de securite)
request.headers) ecrasent les headers d'auth resolus (authHeaders) via l'operateur spread. Un utilisateur peut envoyer un header Authorization custom qui remplace le token resolu.escapedContent est calcule puis ignore. Pass 2 ne couvre pas ce point.escapedContent est calcule sur la ligne 1067, mais la ligne 1068 utilise fileContent (non echappe) dans le heredoc. C'est un bug classique de variable calculee mais pas utilisee.EOFCONNECTORSHUB sur une ligne seule.publicKey et remotePath. Le parametre cwd a ete ignore.options.cwd est injecte dans cd ${options.cwd} && ${command} sans aucun echappement. Un cwd contenant ; rm -rf / serait execute./api/ssh/execute est protege par authentification, mais un utilisateur authentifie peut injecter des commandes via le parametre cwd. Plus grave que l'injection via publicKey car le cwd est un parametre courant.remotePath est injecte dans cat '${remotePath}' sans echapper les single quotes. Une valeur '; rm -rf / ; ' serait injectee.ssh-rsa ${base64} ${comment} mais le base64 est extrait d'un PEM SPKI, pas d'un format SSH. Ce format ne sera pas reconnu par authorized_keys sur la plupart des serveurs SSH.2.3.2, le swagger declare 2.5.0 (index.ts ligne 60), et /health retourne 2.4.0 (index.ts ligne 181)./api/fetch mais pas sur /api/fetch/upload. C'est le meme pattern de non-verification d'auth.getCredential(id, userId?) accepte un userId optionnel (oauth.ts, ligne 40). Si userId est fourni, il est ajoute au filtre eq. Le callback OAuth appelle getCredential(stateData.credential_id) SANS userId (ligne 135), mais verifie ensuite credential.user_id !== stateData.user_id (ligne 137). La protection est presente mais indirecte.new Function() est reellement execute avec acces complet au processus. Pass 2 a ete trop conservative en raison du facteur attenuant (auth requise + container Docker). Cependant, un utilisateur authentifie normal ne devrait PAS pouvoir executer du code cote serveur. Severite retenue : BUG CERTAIN (c'est du RCE, meme si l'auth est requise).generateKeyPairSync() et ne contient pas de single quotes en conditions normales. Le risque est theorique pour le flux actuel, mais reel si un futur changement permet a l'utilisateur de fournir sa propre cle. Severite retenue : BUG PROBABLE (les 2 passes concordent).escapedContent calcule mais fileContent utilisePOST /api/connectors, PUT/DELETE /api/endpoints, GET /api/audit/logs)getUserFromRequest() sans throw (comme POST /api/connectors ligne 1052). Ce n'est pas strictement "sans auth" -- c'est "auth optionnelle". Le finding consolide C-02 distingue les deux cas.Aucun faux positif n'a ete identifie dans les 3 rapports. Tous les findings ont ete verifies dans le code source et correspondent a des problemes reels, meme si leur severite varie.
Le finding de Pass 2 concernant la condition de course sur les API keys (F-09) est un RISQUE THEORIQUE reel mais a impact negligeable (depasser une limite de 10 cles). Ce n'est pas un faux positif, mais il ne merite pas de correction urgente.
| # | Action | Finding | Effort | Impact |
|---|---|---|---|---|
| 1 | Implementer la verification HMAC du JWT avec SUPABASE_JWT_SECRET |
C-01 | 30 min | Elimine l'usurpation d'identite |
| 2 | Ajouter getAuthFromRequest() + reject si null sur tous les endpoints admin (DELETE connectors, POST token, audit logs, etc.) |
C-02 | 1-2h | Elimine l'acces non authentifie aux operations sensibles |
| 3 | Exiger l'authentification sur /api/fetch et /api/fetch/upload | C-03, U-07 | 10 min | Empeche l'utilisation anonyme des tokens systeme |
| 4 | Supprimer NODE_TLS_REJECT_UNAUTHORIZED=0 de conf.prod.gouroubleu.yml |
C-10 | 5 min | Restaure la verification TLS pour les APIs externes |
| 5 | Corriger les appels audit.log() dans index.ts : remplacer snake_case par camelCase et retirer le 2eme argument request |
C-14 | 30 min | Restaure la tracabilite des operations endpoints |
| 6 | Echapper cwd, remotePath et publicKey dans ssh.ts avant injection shell | C-06, U-03, U-04 | 30 min | Elimine 3 vecteurs d'injection de commande SSH |
| 7 | Corriger le heredoc upload : utiliser escapedContent au lieu de fileContent, ou passer en base64 |
U-02 | 15 min | Elimine l'injection via contenu de fichier |
| 8 | Sandboxer les actions Transform (isolated-vm ou worker Bun) ou les restreindre aux admins | C-05 | 2-4h | Elimine le RCE via new Function() |
| 9 | Remplacer getUserFromRequest par getAuthFromRequest sur les endpoints qui l'utilisent (POST connectors, PUT/DELETE endpoints, POST rediscover) |
C-12 | 30 min | Support API keys + enforcement auth |
| 10 | Nettoyer wireguard.ts : supprimer le fichier si vpn-profiles.ts est le remplacement, ou reecrire avec le bon wrapper | C-04 | 1h | Elimine le code mort et la confusion |
| Domaine | Pass 1 | Pass 2 | Pass 3 |
|---|---|---|---|
| JWT / Auth | Excellente | Excellente | Excellente |
| Endpoints sans auth | Bonne | Exhaustive | Bonne |
| SSH injections | publicKey | publicKey | cwd, remotePath, publicKey, heredoc |
| WireGuard API casse | Oui | Oui | Oui |
| Flux de donnees fetch.ts | Basique | En profondeur (headers override) | Basique |
| Actions Transform / RCE | Oui | Oui | Non |
| OAuth state | Oui | Oui | Non |
| Audit.log() appels incorrects | Non | Non | Oui |
| Versions incoherentes | Non | Non | Oui |
| convertToOpenSSH placeholder | Non | Non | Oui |
| TLS global desactive | Oui | Non | Oui |
| Chiffrement sel fixe | Oui | Note | Oui |
Conclusion : Les 3 passes sont complementaires. Pass 1 a la meilleure couverture de surface. Pass 2 a la meilleure profondeur sur les flux d'auth et de donnees. Pass 3 a identifie les bugs d'integration les plus subtils (audit.log snake_case, heredoc variable inutilisee, injections SSH cwd/remotePath). La confrontation des 3 passes permet une confiance elevee sur les findings confirmes et a identifie 8 findings uniques qui auraient ete manques avec une seule passe.