Date: 15/02/2026
Status : IMPLEMENTEE
Auditeur: Claude Opus 4.6
Scope: /stock_8to/33800-stack/projects/connectors-api/src/ (tous fichiers .ts)
Mode: Lecture seule, aucune modification
L'audit de passe 2 a couvert 40 fichiers TypeScript dans le projet connectors-api. L'analyse porte sur les flux d'authentification, les chemins de donnees, la gestion d'etat et la propagation d'erreurs.
Findings critiques: 3 BUG CERTAIN, 4 BUG PROBABLE
Findings importants: 5 RISQUE THEORIQUE
Contexte attenuant: Le service tourne derriere un reverse proxy nginx avec private: true et un sidecar wireguard. Seuls /api/oauth/callback et /health sont publics.
L'authentification repose sur deux mecanismes:
Authorization: Bearer <jwt>)Authorization: Bearer ck_live_*)Le middleware getAuthFromRequest() (middleware/jwt.ts, ligne 59) tente d'abord de detecter une API key, puis valide le JWT.
Fichier: /src/middleware/jwt.ts, lignes 29-52
Severite: BUG CERTAIN
Classification: Contournement d'authentification
// Ligne 30-32: decode SANS verification de signature
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
// Ligne 47: TODO explicite dans le code
// TODO: Implement proper HMAC verification with JWT_SECRET
Analyse: La fonction verifyJWT() decode le payload base64 et verifie uniquement l'expiration (exp). La signature HMAC n'est PAS verifiee malgre la presence de SUPABASE_JWT_SECRET dans l'environnement (conf.prod.gouroubleu.yml).
Impact: Un attaquant disposant d'un acces reseau au service peut forger un JWT valide avec n'importe quel sub (user_id) et acceder a toutes les ressources de n'importe quel utilisateur.
Facteur attenuant: Le service est derriere nginx private: true + wireguard sidecar. L'acces direct est limite au reseau local 192.168.1.x.
Recommendation: Implementer la verification HMAC-SHA256 avec SUPABASE_JWT_SECRET. Bun supporte crypto.createHmac().
Fichier: /src/index.ts
Severite: BUG CERTAIN
Classification: Controle d'acces manquant
Les endpoints suivants n'ont AUCUN appel a getAuthFromRequest():
| Endpoint | Ligne | Methode | Impact |
|---|---|---|---|
POST /api/connectors |
~1050 | Creation connecteur | N'importe qui peut creer des connecteurs |
DELETE /api/connectors/:id |
~1112 | Suppression connecteur | Suppression arbitraire |
POST /api/connectors/:id/provision-type |
~897 | Provisioning type | Modification config |
POST /api/connectors/:id/token |
~1767-1787 | Set SYSTEM token | Critique: permet d'injecter un token systeme |
PUT /api/endpoints/:id |
~1385 | Mise a jour endpoint | Modification metadata |
DELETE /api/endpoints/:id |
~1438 | Suppression endpoint | Suppression metadata |
GET /api/audit/logs |
~1946 | Lecture logs audit | Acces a TOUS les logs de TOUS les users |
GET /api/audit/logs/:id |
~2000 | Lecture log specifique | Acces sans controle |
GET /api/connectors/:id/logs |
~2042 | Logs par connecteur | Acces sans controle |
Le plus critique: POST /api/connectors/:id/token permet de definir un token systeme (api_key, access_token, ou refresh_token) sur n'importe quel connecteur sans aucune authentification. Un attaquant sur le reseau local pourrait remplacer les tokens OAuth de n'importe quel connecteur.
Impact: Sur le reseau local (192.168.1.x), ces endpoints sont accessibles. Le sidecar wireguard protege de l'exterieur uniquement.
Fichier: /src/index.ts, lignes ~201-270
Severite: BUG PROBABLE
Classification: Acces non autorise
// Ligne ~218: auth est optionnel
const auth = await getAuthFromRequest(request.headers);
const userId = auth?.userId || null;
const userJwt = auth?.jwt || null;
// Ligne ~250: execute la requete meme sans userId
const result = await proxyFetch.execute(fetchRequest, userId, userJwt);
Analyse: Si aucun header d'auth n'est fourni, userId est null. La requete est quand meme executee. Dans fetch.ts (ligne 106), sans instanceCredentials et sans userId, le code tombe dans auth.getAuthHeader(connector, userId) qui peut retourner le token systeme du connecteur (fallback ligne 126-131 de auth.ts).
Impact: Un attaquant sur le reseau local peut proxifier des requetes vers n'importe quel connecteur ayant un token systeme configure, SANS s'authentifier.
Facteur attenuant: Concerne seulement les connecteurs avec un token systeme (pas les instances user). Derriere nginx private + wireguard.
Fichier: /src/middleware/jwt.ts, lignes 130-149
Severite: RISQUE THEORIQUE
Classification: Regression de securite potentielle
La fonction getUserFromRequest() est une version synchrone legacy qui appelle verifyJWT() directement. Elle ne peut PAS valider les API keys (qui necessitent un appel async a Supabase). Tout endpoint utilisant cette fonction au lieu de getAuthFromRequest() rejette silencieusement les utilisateurs API key.
Utilise par: POST /api/connectors/:id/rediscover (index.ts ~1125)
Client -> index.ts -> getAuthFromRequest()
-> proxyFetch.execute()
-> registry.get() [lookup connecteur]
-> instances.getByName() [si instance specifiee]
-> oauth.ensureValidToken() [si oauth]
-> auth.getAuthHeader() [fallback]
-> fetch() [requete externe]
-> Response
Fichier: /src/services/fetch.ts, ligne 132
Severite: BUG PROBABLE
Classification: Injection d'en-tetes
const headers: Record<string, string> = {
...authHeaders, // En-tetes d'auth resolues
...request.headers // En-tetes fournies par le client
};
Analyse: Les en-tetes du client (request.headers) sont appliquees APRES les en-tetes d'authentification. Un client peut fournir un header Authorization custom qui ecrase l'auth resolue automatiquement. Ceci permet potentiellement de contourner la resolution d'auth et d'envoyer un token arbitraire a un service externe.
Impact: Un utilisateur authentifie pourrait utiliser les tokens d'un autre utilisateur s'il connait la valeur du token.
Les credentials des instances utilisateurs sont stockees dans la table user_connectors:
api_key: En clair (texte)access_token: En clair (texte)refresh_token: En clair (texte)token_metadata: JSONB, peut contenir des credentials supplementairesFichier: /src/services/instances.ts
Seules les credentials SSH sont chiffrees (AES-256-CBC) dans ssh.ts et vpn-profiles.ts. Les tokens OAuth, API keys, et access tokens sont stockes en plaintext dans Supabase.
Ceci est un choix architectural (pas un bug), mais merite d'etre note.
Client -> index.ts POST /api/ssh/execute
-> getAuthFromRequest() [auth requise]
-> ssh.execute(instanceId, userId, command)
-> instances.get() [verification ownership]
-> decrypt credentials
-> Client SSH2
-> exec(command)
-> Response {stdout, stderr, exitCode}
Fichier: /src/services/ssh.ts, ligne ~997
Severite: BUG PROBABLE
Classification: Injection de commande
// La cle publique est interpolee directement dans la commande shell
const command = `mkdir -p ~/.ssh && echo '${publicKey}' >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys`;
Analyse: Si publicKey contient des caracteres speciaux (apostrophes, backticks, etc.), la commande shell est vulnerable a l'injection. Un utilisateur qui controle la valeur de la cle publique pourrait injecter des commandes arbitraires sur le serveur distant.
Impact: L'endpoint setupAutoKey est protege par authentification, et l'utilisateur fournit lui-meme la cle publique. L'impact est limite car l'utilisateur a deja un acces SSH au serveur (c'est son instance). Mais un frontend compromis pourrait exploiter ceci.
Recommendation: Echapper la valeur ou utiliser un mecanisme SFTP pour ecrire le fichier.
Fichier: /src/services/wireguard.ts, lignes ~153-178
Severite: BUG CERTAIN
Classification: Erreur a l'execution
// Utilise l'API chainee @supabase/supabase-js
const { data, error } = await supabase
.from('user_connectors')
.select('*')
.eq('id', instanceId)
.single();
Analyse: Le wrapper Supabase custom (db/supabase.ts) n'expose PAS de methode .from(). Il expose supabase.select(), supabase.insert(), supabase.update(), etc. Ce code va crasher a l'execution avec TypeError: supabase.from is not a function.
Impact: Toute tentative de connexion/deconnexion VPN via WireGuard va echouer. Les endpoints /api/vpn/connect, /api/vpn/disconnect, et les fonctions internes de wireguard.ts qui utilisent cette syntaxe sont non-fonctionnels.
Recommandation: Migrer vers supabase.select('user_connectors', { eq: { id: instanceId }, single: true }).
Client -> POST /api/ai/conversations/:id/message
-> getAuthFromRequest() [auth requise]
-> aiChat.sendMessage() / sendMessageStream()
-> _processMessage()
-> detectIntent() [deterministe, pas de LLM]
-> intentClassifier.validateIntent() [LLM optionnel]
-> executeIntent() / executeToolCall()
-> proxyFetch.execute() [via userId]
-> actions.execute() [via userId]
-> submitJob() [via ai-orchestrator]
-> buildOllamaMessages()
-> submitJob() [chat LLM]
-> saveMessage() -> Supabase
-> SSE Response
Fichier: /src/services/actions.ts, lignes 852-853
Severite: RISQUE THEORIQUE
Classification: Execution de code arbitraire
const fn = new Function('input', 'ctx', `"use strict";\nreturn (async () => {\n${config.code}\n})()`);
const result = await Promise.race([fn(input, ctx), ...]);
Analyse: Les actions de type transform executent du code JavaScript arbitraire fourni par l'utilisateur via new Function(). Le contexte ctx expose executeAction() (peut executer d'autres actions de l'utilisateur) et fetch() (peut appeler des APIs via proxyFetch). "use strict" n'est PAS un sandbox -- le code a acces au scope global de Bun (process, Bun, require, etc.).
Impact: Un utilisateur authentifie peut executer du code arbitraire dans le processus Node/Bun du serveur. Ceci donne acces aux variables d'environnement (secrets, tokens), au filesystem, et a toutes les ressources du container.
Facteur attenuant: L'utilisateur doit etre authentifie. Le service tourne dans un container Docker isole. Le timeout empeche les boucles infinies.
Recommandation: Utiliser un worker Bun ou un isolat V8 avec une whitelist de globals.
Fichier: /src/services/apiKeys.ts, lignes 159-183
Severite: RISQUE THEORIQUE
Classification: TOCTOU (Time-of-check to time-of-use)
// Ligne 159-163: Comptage des cles existantes
const existing = await supabase.select('api_keys', {
columns: 'id', eq: { user_id: userId }, is: { revoked_at: null }
});
// Ligne 165-167: Verification du max
if (existing.data && existing.data.length >= MAX_KEYS_PER_USER) {
return { success: false, error: '...' };
}
// ... verification du nom (lignes 170-177) ...
// Ligne 183: Insertion
const result = await supabase.insert('api_keys', { ... });
Analyse: Entre le SELECT de comptage (ligne 159) et l'INSERT (ligne 183), un autre request concurrent peut creer une cle. Deux requetes simultanees pourraient depasser la limite de 10 cles.
Impact: Faible. Permet de depasser la limite de cles, pas un vecteur d'attaque.
Fichier: /src/services/oauth.ts, lignes ~99-103
Severite: RISQUE THEORIQUE
Classification: CSRF sur le flux OAuth
// Le state est juste du JSON encode en base64
const state = Buffer.from(JSON.stringify({
connector_type: connectorType,
user_id: userId,
redirect_uri: redirectUri
})).toString('base64url');
Analyse: Le parametre state OAuth n'est ni signe (HMAC) ni chiffre. Un attaquant qui connait l'user_id pourrait forger un state valide et realiser une attaque CSRF sur le callback OAuth.
Facteur attenuant: Le callback /api/oauth/callback est un des seuls endpoints publics (conf.prod.gouroubleu.yml, public_paths). Toutefois, l'attaquant devrait aussi controler le code d'autorisation.
Fichier: /src/services/oauth.ts, ligne ~40
Severite: RISQUE THEORIQUE
Classification: IDOR (Insecure Direct Object Reference)
async getCredential(credentialId: string): Promise<OAuthCredential | null> {
const result = await supabase.select('oauth_credentials', {
eq: { id: credentialId }, single: true
});
return result.data;
}
Analyse: La fonction getCredential() ne prend pas de userId en parametre et ne verifie pas la propriete du credential. Tout code interne qui appelle cette fonction avec un ID de credential peut acceder aux credentials OAuth de n'importe quel utilisateur.
Impact: En pratique, credentialId vient de instance.oauth_credential_id qui est filtre par userId dans les instances. Mais un bug dans la resolution d'instance pourrait exposer des credentials.
Le projet utilise deux patterns d'erreur:
{ success: false, error: message } pour les endpoints RESTFichier: /src/services/audit.ts, lignes ~126-129
Severite: RISQUE THEORIQUE (par design)
Classification: Perte silencieuse de logs d'audit
try {
await supabase.insert('audit_logs', logData);
} catch (e) {
console.error('[Audit] Failed to log:', e);
}
Analyse: Les erreurs d'insertion de logs d'audit sont capturees et loguees sur console, mais n'affectent jamais l'operation principale. C'est intentionnel (fire-and-forget) mais signifie qu'une panne Supabase pourrait entrainter la perte de traces d'audit.
Le service SSH (ssh.ts) propage les erreurs correctement:
{ success: false, error: 'Connection failed: ...' }{ success: false, error: 'Command timed out' }{ success: false, stderr: '...' }Le packageDetector.ts analyse ensuite le stderr pour detecter les outils manquants et proposer des commandes d'installation.
Le service OAuth propage les erreurs HTTP depuis les fournisseurs:
response.statusTextCertaines operations sont intentionnellement fire-and-forget:
apiKeys.ts ligne 112: Mise a jour de last_used_at sur validationaudit.ts: Toutes les insertions d'auditaiChat.ts lignes ~1106-1108: Mise a jour d'attachmentssaveToMemory() dans aiChat.ts: Best-effortFichier: /src/services/wireguard.ts, lignes ~470-486
Severite: BUG PROBABLE
Classification: Fuite d'information
Analyse: La methode listActive() ne filtre PAS par userId. Elle retourne l'etat de TOUS les tunnels WireGuard actifs sur le sidecar, quel que soit l'utilisateur qui appelle. L'endpoint /api/vpn/active (index.ts ~3506) passe auth.userId mais la fonction wireguard.listActive() ne l'utilise possiblement pas pour filtrer.
Impact: Un utilisateur authentifie peut voir les tunnels VPN actifs des autres utilisateurs.
| ID | Description | Fichier | Ligne |
|---|---|---|---|
| F-01 | Signature JWT non verifiee | middleware/jwt.ts | 29-52 |
| F-02 | Endpoints admin sans auth | index.ts | Multiples |
| F-07 | wireguard.ts API Supabase inexistante | services/wireguard.ts | 153-178 |
| ID | Description | Fichier | Ligne |
|---|---|---|---|
| F-03 | /api/fetch accessible anonymement | index.ts | ~218 |
| F-05 | Override d'en-tetes auth par le client | services/fetch.ts | 132 |
| F-06 | Injection commande SSH dans setupAutoKey | services/ssh.ts | ~997 |
| F-13 | listActive() VPN sans filtrage user | services/wireguard.ts | ~470-486 |
| ID | Description | Fichier | Ligne |
|---|---|---|---|
| F-04 | getUserFromRequest ne valide pas API keys | middleware/jwt.ts | 130-149 |
| F-08 | executeTransform: code JS sans sandbox | services/actions.ts | 852-853 |
| F-09 | Race condition creation API keys | services/apiKeys.ts | 159-183 |
| F-10 | OAuth state non signe | services/oauth.ts | ~99-103 |
| F-11 | getCredential sans verification owner | services/oauth.ts | ~40 |
| F-12 | Logs audit fire-and-forget | services/audit.ts | ~126-129 |
| Methode | Path | Auth? | Remarque |
|---|---|---|---|
| POST | /api/fetch | Non (optionnel) | F-03 |
| POST | /api/connectors | Non | F-02 |
| DELETE | /api/connectors/:id | Non | F-02 |
| POST | /api/connectors/:id/provision-type | Non | F-02 |
| POST | /api/connectors/:id/token | Non | F-02 CRITIQUE |
| PUT | /api/endpoints/:id | Non | F-02 |
| DELETE | /api/endpoints/:id | Non | F-02 |
| GET | /api/audit/logs | Non | F-02 |
| GET | /api/audit/logs/:id | Non | F-02 |
| GET | /api/connectors/:id/logs | Non | F-02 |
| GET | /api/connectors | Non | Listing public, acceptable |
| GET | /api/connectors/:id | Non | Lecture publique, acceptable |
| GET | /health | Non | Par design |
| Methode | Path | Auth | Scope check |
|---|---|---|---|
| POST | /api/auth/login | N/A | Cree la session |
| POST | /api/auth/register | N/A | Cree le user |
| GET | /api/user/instances | JWT/API key | userId filtre |
| POST | /api/user/instances | JWT/API key | userId filtre |
| PUT | /api/user/instances/:id | JWT/API key | userId + ownership |
| DELETE | /api/user/instances/:id | JWT/API key | userId + ownership |
| POST | /api/ssh/execute | JWT/API key | userId + instance ownership |
| GET | /api/actions | JWT/API key | userId filtre |
| POST | /api/actions | JWT/API key | userId injecte |
| PUT | /api/actions/:id | JWT/API key | userId + ownership |
| DELETE | /api/actions/:id | JWT/API key | userId + ownership |
| POST | /api/actions/:id/execute | JWT/API key | userId + access |
| GET | /api/api-keys | JWT | userId filtre |
| POST | /api/api-keys | JWT | userId filtre |
| DELETE | /api/api-keys/:id | JWT | userId + ownership |
| GET | /api/ai/conversations | JWT/API key | userId filtre |
| POST | /api/ai/conversations | JWT/API key | userId injecte |
| POST | /api/ai/conversations/:id/message | JWT/API key | userId + ownership |
| GET | /api/routines | JWT/API key | userId filtre |
| POST | /api/routines | JWT/API key | userId injecte |
| POST | /api/routines/:id/execute | JWT/API key | userId + ownership |
| GET | /api/vpn/profiles | JWT/API key | userId filtre |
| POST | /api/vpn/profiles | JWT/API key | userId injecte |
| PUT | /api/vpn/profiles/:id | JWT/API key | userId + ownership |
| DELETE | /api/vpn/profiles/:id | JWT/API key | userId + ownership |
Le systeme de scopes (SCOPES dans apiKeys.ts) est defini mais n'est PAS verifie sur les endpoints. hasScope() et hasAnyScope() sont exportes mais aucun endpoint dans index.ts n'appelle ces fonctions pour verifier que la cle API a le scope requis. Les scopes sont stockes mais non enforces.
Le chiffrement des credentials SSH utilise AES-256-CBC avec:
SSH_ENCRYPTION_KEY depuis l'environnement (fallback hardcode: 'default-key-change-in-production-32!' dans ssh.ts ligne 105)'salt' dans scryptSync (ssh.ts ligne 109)Le meme pattern est duplique dans wireguard.ts et vpn-profiles.ts. En production, la cle vient des secrets Docker (SSH_ENCRYPTION_KEY dans conf.prod.gouroubleu.yml), donc le fallback hardcode ne devrait pas etre utilise. Mais le salt statique affaiblit le KDF.
fetch.ts lignes 171-173 desactive la verification TLS pour les IPs 192.168.* et 10.*:
const tlsOptions = url.startsWith('https://192.168.') || url.startsWith('https://10.')
? { tls: { rejectUnauthorized: false } }
: {};
Ceci est justifie par les certificats auto-signes de services internes (ex: Proxmox), mais desactive la protection MITM sur le reseau local.
ssh.ts ligne ~943: hostVerifier retourne toujours true. Aucune verification de cle d'hote SSH n'est effectuee, ce qui rend le service vulnerable au MITM sur les connexions SSH.
db/supabase.ts ligne 9: Toutes les operations base de donnees utilisent SUPABASE_SERVICE_KEY (cle de service avec acces complet). Il n'y a pas de RLS (Row Level Security) cote serveur -- toute la logique de filtrage par userId est dans le code applicatif TypeScript.