33800 Docs

← Retour

AUDIT PASSE 2 - connectors-api

Flux de donnees, authentification, etat, propagation d'erreurs

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


TABLE DES MATIERES

  1. Resume executif
  2. Authentification et autorisation
  3. Flux de donnees
  4. Gestion d'etat
  5. Propagation d'erreurs
  6. Inventaire des findings
  7. Matrice des endpoints

1. RESUME EXECUTIF

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.


2. AUTHENTIFICATION ET AUTORISATION

2.1 Architecture d'auth

L'authentification repose sur deux mecanismes:

  1. JWT Supabase (via header Authorization: Bearer <jwt>)
  2. API Keys (via header Authorization: Bearer ck_live_*)

Le middleware getAuthFromRequest() (middleware/jwt.ts, ligne 59) tente d'abord de detecter une API key, puis valide le JWT.

2.2 FINDING F-01: Signature JWT non verifiee

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().


2.3 FINDING F-02: Endpoints admin sans authentification

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.


2.4 FINDING F-03: Acces anonyme a /api/fetch

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.


2.5 FINDING F-04: getUserFromRequest synchrone ne valide pas les API keys

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)


3. FLUX DE DONNEES

3.1 Flux /api/fetch (proxy principal)

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

3.2 FINDING F-05: Override d'en-tetes auth par le client

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.


3.3 Flux de stockage des credentials

Les credentials des instances utilisateurs sont stockees dans la table user_connectors:

Fichier: /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.

3.4 Flux SSH

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}

3.5 FINDING F-06: Injection de commande SSH dans setupAutoKey

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.


3.6 FINDING F-07: wireguard.ts utilise une API Supabase inexistante

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 }).


3.7 Flux AI Chat

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

3.8 FINDING F-08: executeTransform execute du code JS arbitraire

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.


4. GESTION D'ETAT

4.1 FINDING F-09: Condition de course sur la creation d'API keys

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.

4.2 FINDING F-10: OAuth state non signe cryptographiquement

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.

4.3 FINDING F-11: getCredential sans verification de propriete

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.


5. PROPAGATION D'ERREURS

5.1 Pattern general

Le projet utilise deux patterns d'erreur:

  1. Return { success: false, error: message } pour les endpoints REST
  2. Throw Error pour les services internes (catch au niveau route)

5.2 FINDING F-12: Erreurs Supabase swallowed dans l'audit

Fichier: /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.

5.3 Pattern d'erreur SSH

Le service SSH (ssh.ts) propage les erreurs correctement:

Le packageDetector.ts analyse ensuite le stderr pour detecter les outils manquants et proposer des commandes d'installation.

5.4 Pattern d'erreur OAuth

Le service OAuth propage les erreurs HTTP depuis les fournisseurs:

5.5 Erreurs fire-and-forget

Certaines operations sont intentionnellement fire-and-forget:

5.6 FINDING F-13: listActive() dans wireguard retourne tous les tunnels

Fichier: /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.


6. INVENTAIRE DES FINDINGS

BUG CERTAIN (3)

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

BUG PROBABLE (4)

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

RISQUE THEORIQUE (5)

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

7. MATRICE DES ENDPOINTS

Endpoints SANS authentification (decouverts)

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

Endpoints AVEC authentification correcte

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

Notes sur les scopes API key

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.


NOTES COMPLEMENTAIRES

Chiffrement SSH

Le chiffrement des credentials SSH utilise AES-256-CBC avec:

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.

TLS interne

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 Host Verification

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.

Supabase Service Key

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.


FIN DE L'AUDIT PASSE 2