Date : 15/02/2026
Status : IMPLEMENTEE
Auditeur : Claude Opus 4.6 (pass independante)
Projet : /stock_8to/33800-stack/projects/connectors-api/
Scope : Contrats API, Supabase, SSH, WireGuard, Auth flow, Proxy /api/fetch
| Severite | Nombre |
|---|---|
| BUG CERTAIN | 5 |
| BUG PROBABLE | 6 |
| RISQUE THEORIQUE | 5 |
| TOTAL | 16 |
Les problemes les plus critiques :
.from().select().eq().single() n'existe pas dans le wrapper customcwd, remotePath, et sshPublicKeyescapedContent calcule mais jamais utilisesrc/middleware/jwt.ts:47verifyJWT() decode le payload JWT en base64 mais ne verifie JAMAIS la signature HMAC. Un commentaire TODO confirme : // TODO: Implement proper HMAC verification with JWT_SECRET. Un attaquant sur le reseau local peut forger un JWT avec n'importe quel sub (user_id) et acceder a toutes les ressources de l'utilisateur cible.// jwt.ts:47 - Decode sans verification
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
// Verifie seulement l'expiration, PAS la signature
if (payload.exp && payload.exp < Date.now() / 1000) {
return null;
}
return payload;
private: true + sidecar WireGuard en prod (seuls /api/oauth/callback et /health sont publics).JWT_SECRET en utilisant crypto.createHmac().src/services/wireguard.ts:153-178 (et dans toutes les fonctions)supabase.from('user_connectors').select().eq().single() — c'est l'API de @supabase/supabase-js. Or le projet utilise un wrapper custom (src/db/supabase.ts) qui exporte supabase.select(), supabase.insert(), etc. La methode .from() n'existe PAS sur le wrapper custom. Toutes les fonctions WireGuard crashent a l'execution avec TypeError: supabase.from is not a function.
// wireguard.ts - API inexistante
const { data } = await supabase
.from('user_connectors')
.select('*')
.eq('id', instanceId)
.eq('user_id', userId)
.single();
// supabase.ts - API reelle export const supabase = { select(table, options), insert(table, data), update(table, data, match), delete(table, match), // PAS de .from() };
- **Impact** : Toutes les operations VPN WireGuard (generateKeys, configure, connect, disconnect, getStatus, listActive, etc.) sont cassees. Le VPN ne fonctionne pas du tout.
- **Fix suggere** : Reecrire wireguard.ts pour utiliser `supabase.select('user_connectors', { filter: ... })` au lieu du chainage `.from()`.
---
### F-03 : 10+ endpoints sans authentification
- **Severite** : BUG CERTAIN
- **Fichier** : `src/index.ts` (multiples lignes)
- **Description** : Les endpoints suivants n'appellent jamais `getAuthFromRequest()` et sont accessibles sans token :
- `DELETE /api/connectors/:id` (ligne 1112) — supprime un connecteur
- `POST /api/connectors/:id/token` (ligne 1767) — definit un token systeme
- `POST /api/connectors/:id/provision-type` (ligne 897) — change le type de provision
- `GET /api/connectors/:id/schema` (ligne 1183) — lit le schema
- `GET /api/connectors/:id/openapi` (ligne 1219) — lit l'OpenAPI spec
- `GET /api/connectors/:id/endpoints` (ligne 1240) — lit les endpoints
- `GET /api/endpoints/:id` (ligne 2072) — lit un endpoint
- `GET /api/endpoints/:id/versions` (ligne 2088) — lit les versions
- `GET /api/audit/logs/:id` (ligne 2000) — lit n'importe quel audit log
- `GET /api/connectors/:id/logs` (ligne 2042) — lit les logs d'un connecteur
- **Code concerne** :
```typescript
// Exemple: DELETE sans auth
.delete('/api/connectors/:id', async ({ params }) => {
// PAS de getAuthFromRequest() ici
const result = await registry.deleteConnector(params.id);
return result;
})
const auth = await getAuthFromRequest(request.headers); if (!auth) throw new Error('Authentication required'); a chaque endpoint.src/services/ssh.ts:1067-1068escapedContent = fileContent.replace(/'/g, "'\\\\''") mais utilise ensuite fileContent (non echappe) dans le heredoc. Le contenu du fichier est injecte tel quel.const escapedContent = fileContent.replace(/'/g, "'\\\\''" );
// ^^^ calcule mais JAMAIS utilise
let command = `cat > '${remotePath}' << 'EOFCONNECTORSHUB'\n${fileContent}\nEOFCONNECTORSHUB`;
// ^^^ utilise fileContent au lieu de escapedContent
EOFCONNECTORSHUB sur une ligne seule, le heredoc se ferme prematurement et le reste du contenu est interprete comme commande shell. Injection de commande via contenu de fichier.escapedContent au lieu de fileContent, ou mieux : encoder en base64 et decoder cote serveur.src/services/ssh.ts:205-207cwd est injecte directement dans la commande shell sans aucun echappement ni validation. Un utilisateur authentifie peut executer des commandes arbitraires en passant un cwd malicieux.if (options?.cwd) {
fullCommand = `cd ${options.cwd} && ${command}`;
// options.cwd = "; rm -rf /" -> fullCommand = "cd ; rm -rf / && ..."
}
fullCommand = \cd ${shellEscape(options.cwd)} && ${command}``src/services/ssh.ts:1106-1108remotePath est injecte directement dans des commandes shell comme cat '${remotePath}'. Les single quotes dans le path ne sont pas echappees.let command = `cat '${remotePath}'`;
// remotePath = "'; rm -rf / ; '" -> commande injectee
remotePath.replace(/'/g, "'\\\\''").src/services/ssh.ts:997echo '...' sans echappement. Si la cle contient des single quotes (improbable pour une cle generee mais possible si la cle est fournie par l'utilisateur), c'est une injection.const installCommand = `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '${sshPublicKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
src/index.ts:201-204/api/fetch est le point d'entree principal pour acceder a tous les services. Il accepte les requetes sans token d'authentification : const userId = auth?.userId || null. Avec un userId null, il utilise les tokens systeme des connecteurs pour authentifier les requetes vers les services cibles.const auth = await getAuthFromRequest(request.headers);
const userId = auth?.userId || null;
// Pas de rejet si userId est null — utilise les tokens systeme
/api/fetch pour acceder aux services via les tokens systeme des connecteurs (GitLab, Supabase, etc.).if (!auth) return { success: false, error: 'Authentication required' };src/index.ts:1352-1359, 1405-1412, 1448-1455AuditLogInput utilise des proprietes camelCase (userId, connectorId, resourceId), mais certains appels dans index.ts passent des proprietes snake_case (user_id, connector_id, resource_id). De plus, audit.log() prend un seul argument AuditLogInput, mais certains appels passent un deuxieme argument request.
// Appel incorrect (snake_case + extra arg):
audit.log({
user_id: userId, // devrait etre userId
connector_id: params.id, // devrait etre connectorId
action: 'connector.updated',
resource_id: params.id, // devrait etre resourceId
}, request); // 2eme arg non supporte
// Interface correcte: interface AuditLogInput { userId: string; connectorId?: string; resourceId?: string; action: string; // ... }
- **Impact** : Les logs d'audit ne contiennent pas le userId, connectorId, ni resourceId car les proprietes snake_case sont ignorees par l'interface. Les logs sont incomplets.
- **Fix suggere** : Utiliser les bons noms de proprietes (camelCase) et retirer le deuxieme argument.
---
### F-10 : Chiffrement avec sel statique "salt"
- **Severite** : BUG PROBABLE
- **Fichier** : `src/services/ssh.ts:109`, `src/services/wireguard.ts:78`, `src/services/vpn-profiles.ts:78`
- **Description** : Les trois fichiers utilisent `crypto.scryptSync(ENCRYPTION_KEY, 'salt', 32)` avec un sel fixe. Le sel statique reduit la resistance de scrypt aux attaques par dictionnaire — toutes les cles derivees avec la meme ENCRYPTION_KEY produisent le meme resultat.
- **Code concerne** :
```typescript
const key = crypto.scryptSync(ENCRYPTION_KEY, 'salt', 32);
src/services/ssh.ts:1033-1043convertToOpenSSH est commentee comme "placeholder that won't work with all keys". Elle genere un format PEM SPKI au lieu du format OpenSSH (ssh-rsa ...). L'installation de la cle publique via setupAutoKey pourrait echouer si le serveur SSH n'accepte pas le format PEM.// This is a placeholder that won't work with all keys
// For production, use ssh-keygen or a proper library
convertToOpenSSH(publicKey: string): string {
return publicKey; // Returns PEM format, NOT OpenSSH format
}
authorized_keys.ssh-keygen -y -f private.pem ou une librairie comme sshpk pour generer le format OpenSSH.package.json:3, src/index.ts (health + swagger)package.json dit 2.3.2, l'endpoint /health retourne 2.4.0, et la documentation Swagger declare 2.5.0.import pkg from '../package.json'; const VERSION = pkg.version;src/middleware/jwt.tsgetUserFromRequest() est synchrone et ne peut donc pas valider les API keys (qui necessite une requete DB asynchrone). Elle est utilisee dans requireAuth() via le middleware Elysia. Les utilisateurs authentifies par API key sont rejetes par les endpoints qui utilisent requireAuth() au lieu de getAuthFromRequest().
// Sync - ne supporte que JWT
function getUserFromRequest(request): { userId: string } | null {
// Ne peut pas faire await apiKeys.verify(...)
}
// Async - supporte JWT + API keys async function getAuthFromRequest(headers): Promise<{ userId: string } | null> { // Peut faire await apiKeys.verify(...) }
- **Impact** : Certains endpoints rejettent les utilisateurs avec API key s'ils utilisent le middleware sync au lieu de la fonction async.
- **Fix suggere** : Convertir `requireAuth()` en middleware async, ou documenter quels endpoints supportent les API keys.
---
### F-14 : AI webhook secret optionnel
- **Severite** : RISQUE THEORIQUE
- **Fichier** : `src/index.ts:4936-5045`
- **Description** : Le webhook AI verifie le secret uniquement si `AI_WEBHOOK_SECRET` est defini dans les variables d'environnement. Si la variable n'est pas definie, n'importe quelle requete est acceptee.
- **Code concerne** :
```typescript
if (expectedSecret && secret !== expectedSecret) {
// Si expectedSecret est vide/undefined, la condition est false -> requete acceptee
return { success: false, error: 'Invalid secret' };
}
conf.prod.gouroubleu.ymlNODE_TLS_REJECT_UNAUTHORIZED=0 desactive la verification des certificats TLS pour TOUTES les connexions HTTPS sortantes du container. Cela inclut les appels vers les APIs externes (GitLab, Mailjet, GitHub, etc.).src/index.ts:334/api/fetch/upload accepte les requetes sans verifier l'authentification, similaire a /api/fetch. Cela permet a un utilisateur non authentifie de faire des uploads via les tokens systeme des connecteurs.private: true.t.Object) bien definis pour le body, les params, et les reponsessupabase.ts fonctionne correctement pour les operations CRUDvpn-profiles.ts utilise correctement le wrapper custom (coherent)getAuthForInstance()