Date : 15/02/2026 Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 Projet : connectors-api (Bun + Elysia + TypeScript + Supabase PostgREST) Methode : Lecture integrale de chaque fichier source (39 fichiers .ts), tracage des chemins d'execution reels Fichiers audites : index.ts (~5700 lignes), 22 services, 3 types, 1 data, 1 middleware, 1 db
| Severite | Nombre |
|---|---|
| CRITIQUE | 2 |
| HAUTE | 7 |
| MOYENNE | 6 |
| BASSE | 4 |
| TOTAL | 19 |
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/middleware/jwt.ts, lignes 29-52
Certitude : CERTAIN
Code :
export function verifyJWT(token: string): JWTPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
// Check expiration
if (payload.exp && payload.exp < Date.now() / 1000) return null;
// For production, we should verify the signature
// but for now we trust tokens that decode properly
// TODO: Implement proper HMAC verification with JWT_SECRET
return payload;
} catch {
return null;
}
}
Analyse :
La fonction verifyJWT() decode le payload base64 du JWT mais ne verifie JAMAIS la signature HMAC. Le commentaire "TODO" confirme que c'est intentionnel. N'importe qui peut forger un JWT avec un sub (user_id) arbitraire. Il suffit de construire un JWT sans signature valide, et la fonction le considere comme authentique.
Cette fonction est appelee par getUserFromRequest() (ligne 67) et getAuthFromRequest() (ligne 80) qui sont utilises dans ~100 endpoints de index.ts.
Impact : Usurpation d'identite totale. Un attaquant peut agir en tant que n'importe quel utilisateur en forgeant un JWT {"sub":"uuid-victime"}.
Facteur attenuant : Le nginx reverse-proxy est configure en private: true (conf.prod.gouroubleu.yml ligne 33), ce qui restreint l'acces reseau aux IPs autorisees. L'exploitation necessite un acces au reseau local ou une IP whitelistee. Cependant, les endpoints /api/oauth/callback et /health sont dans public_paths et sont accessibles depuis internet.
Fix propose :
import { createHmac, timingSafeEqual } from 'crypto';
export function verifyJWT(token: string): JWTPayload | null {
try {
const [headerB64, payloadB64, signatureB64] = token.split('.');
if (!headerB64 || !payloadB64 || !signatureB64) return null;
const secret = process.env.JWT_SECRET || process.env.SUPABASE_JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET not configured');
const expectedSig = createHmac('sha256', secret)
.update(`${headerB64}.${payloadB64}`)
.digest('base64url');
const sigBuffer = Buffer.from(signatureB64, 'base64url');
const expectedBuffer = Buffer.from(expectedSig, 'base64url');
if (sigBuffer.length !== expectedBuffer.length || !timingSafeEqual(sigBuffer, expectedBuffer)) {
return null;
}
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
if (payload.exp && payload.exp < Date.now() / 1000) return null;
return payload;
} catch {
return null;
}
}
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/index.ts
Certitude : CERTAIN
Endpoints affectes (aucun appel a getAuthFromRequest, getUserFromRequest, ou requireAuth) :
| Ligne | Endpoint | Risque |
|---|---|---|
| 1112 | DELETE /api/connectors/:id |
Supprimer n'importe quel connecteur |
| 1767 | POST /api/connectors/:id/token |
Injecter un token systeme sur n'importe quel connecteur |
| 897 | POST /api/connectors/:id/provision-type |
Provisionner des endpoints |
| 1183 | GET /api/connectors/:id/schema |
Lire le schema (information disclosure) |
| 1219 | GET /api/connectors/:id/openapi |
Lire les specs OpenAPI |
| 2000 | GET /api/audit/logs/:id |
Lire n'importe quel log d'audit |
| 2042 | GET /api/connectors/:id/logs |
Lire les logs d'un connecteur |
Analyse :
Le POST /api/connectors/:id/token (ligne 1767) est le plus dangereux : il permet de definir un token systeme sans aucune authentification. Code exact :
.post('/api/connectors/:id/token', async ({ params, body }) => {
try {
const connector = await registry.get(params.id);
if (!connector) {
return { error: 'Connector not found' };
}
await auth.setToken(connector, body.token, body.type || 'api_key');
return { success: true, message: `System token set for ${connector.name}` };
} catch (err: any) {
return { error: err.message };
}
})
Et DELETE /api/connectors/:id (ligne 1112) :
.delete('/api/connectors/:id', async ({ params }) => {
try {
await registry.delete(params.id);
return { success: true };
} catch (err: any) {
return { error: err.message };
}
})
Facteur attenuant : nginx private: true protege au niveau reseau. Mais ces endpoints restent dangereux si un utilisateur authentifie du reseau local les appelle (pas de verification de droits admin).
Fix propose : Ajouter requireAuth() ou un middleware admin sur chaque endpoint sensible. Au minimum :
.post('/api/connectors/:id/token', async ({ params, body, request }) => {
const auth = await getAuthFromRequest(request.headers);
if (!auth) throw new Error('Authentication required');
// + check isAdmin(auth.userId)
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/index.ts, lignes 201-216
Certitude : CERTAIN
Code :
.post('/api/fetch', async ({ body, request }) => {
const auth = await getAuthFromRequest(request.headers);
const userId = auth?.userId || null;
// ...
const result = await proxyFetch.execute({
connector: body.connector,
instance: body.instance,
method: body.method,
path: body.path,
body: body.body,
params: body.params,
headers: body.headers
}, userId || undefined, auth?.token);
Analyse :
getAuthFromRequest() retourne null si aucun token n'est present. Le code ne fait pas de return 401 mais continue avec userId = null. Le proxyFetch.execute() recoit userId = undefined et tente quand meme d'executer la requete avec les tokens systeme (fallback dans auth.getAuthHeader() qui cherche d'abord le token user, puis le token systeme sans user_id).
Un appel anonyme a /api/fetch peut donc utiliser les tokens systeme des connecteurs.
Facteur attenuant :
private: true bloque l'acces externe/api/fetch n'est PAS dans public_pathsImpact moyen en pratique car la protection reseau est effective. Mais c'est un defaut de defense-in-depth.
Fix propose :
.post('/api/fetch', async ({ body, request }) => {
const auth = await getAuthFromRequest(request.headers);
if (!auth) {
return { success: false, status: 401, error: 'Authentication required' };
}
const userId = auth.userId;
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/wireguard.ts
Certitude : CERTAIN (bug, pas vulnerabilite directe)
Code (lignes representant le pattern repete dans toutes les fonctions) :
const { data: instance, error: fetchError } = await supabase
.from('user_connectors')
.select('token_metadata')
.eq('id', instanceId)
.eq('user_id', userId)
.single();
Analyse :
Le wrapper Supabase (src/db/supabase.ts) N'EXPOSE PAS la methode .from(). Il expose uniquement supabase.select(), supabase.insert(), supabase.update(), supabase.delete(), supabase.rpc(), supabase.count(), et supabase.storage.*.
L'appel supabase.from(...) va lancer une erreur TypeError: supabase.from is not a function a l'execution. Ce pattern est present dans TOUTES les fonctions de wireguard.ts :
getWireguardConfig() (ligne ~153)updateWireguardConfig() (ligne ~233)generateKeys() (ligne ~317)activateVPN() (ligne ~400)deactivateVPN() (ligne ~470)getVPNStatus() (ligne ~530)getFullConfig() (ligne ~580)Impact : Toutes les fonctions wireguard.ts crashent a l'execution. Le service VPN est inoperant via ce fichier.
Note : vpn-profiles.ts utilise le bon wrapper et semble etre le remplacement fonctionnel. Les endpoints VPN dans index.ts (lignes 3274+) appellent directement vpn-profiles.ts et fonctionnent.
Fix propose : Reecrire wireguard.ts pour utiliser le wrapper correct, ou supprimer le fichier si vpn-profiles.ts l'a entierement remplace.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/actions.ts, ligne ~853
Certitude : CERTAIN
Code :
const fn = new Function('input', 'ctx', `"use strict";\nreturn (async () => {\n${config.code}\n})()`);
L'objet ctx fourni a la fonction contient :
const ctx = {
executeAction: ..., // peut executer d'autres actions
fetch: ..., // proxyFetch avec auth
instances: ..., // liste des instances
actions: ..., // liste des actions
};
Analyse :
Un utilisateur authentifie peut creer une action de type transform avec du code JavaScript arbitraire execute cote serveur dans le processus Bun. Le new Function() n'est PAS sandboxe : le code a acces a process, require (via import dynamique), au filesystem, etc. L'objet ctx donne en plus l'acces a proxyFetch (appels API avec credentials) et executeAction (execution d'autres actions).
Exemples d'exploitation :
// Lire les variables d'environnement (credentials)
return process.env;
// Executer des commandes systeme
const { execSync } = require('child_process');
return execSync('cat /etc/passwd').toString();
// Acceder au filesystem
const fs = require('fs');
return fs.readFileSync('/proc/self/environ').toString();
Impact : RCE (Remote Code Execution) pour tout utilisateur authentifie. Acces total au processus serveur.
Fix propose : Utiliser un sandbox (vm2, isolated-vm) ou restreindre les transforms a des operations predefinies (comme le fait deja routineExecutor.ts avec ses TransformOp typees). Alternativement, limiter les actions transform aux administrateurs.
Fichier(s) : Multiples Certitude : CERTAIN
| Fichier | Ligne | IP hardcodee | Usage |
|---|---|---|---|
src/services/ai.ts |
159 | http://192.168.1.12:5501 |
Fallback downloadJobFile |
src/services/notify.ts |
5 | http://192.168.1.12:5300 |
Fallback NOTIF_URL |
src/db/supabase.ts |
8 | http://192.168.1.12:8000 |
Fallback SUPABASE_URL |
src/services/userAuth.ts |
6 | http://supabase-kong-prod:8000 |
Fallback SUPABASE_AUTH_URL (Docker hostname, pas une IP - OK) |
Analyse :
Selon les regles (CLAUDE.md interdit les IPs dans le code applicatif), ces fallbacks ne devraient pas exister. En production, les variables d'environnement sont injectees par conf.prod.gouroubleu.yml, donc les fallbacks ne sont normalement pas utilises. Cependant :
*.33800.nowhere84.com en local - les IPs sont donc inutilesFix propose : Remplacer les fallbacks IP par des domaines *.33800.nowhere84.com ou lever une erreur si l'env var est manquante.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/oauth.ts et src/index.ts ligne 1917
Certitude : CERTAIN
Code dans oauth.ts :
getAuthorizeUrl(credential: OAuthCredential): string {
const state = Buffer.from(JSON.stringify({
credential_id: credential.id,
user_id: credential.user_id,
nonce: randomBytes(16).toString('hex')
})).toString('base64');
// ...
}
Code dans le callback (index.ts ligne 1929) :
await oauth.handleCallback(code, state);
Dans oauth.handleCallback() :
const stateData = JSON.parse(Buffer.from(state, 'base64').toString());
const credential = await this.getCredential(stateData.credential_id, stateData.user_id);
Analyse :
Le state encode credential_id, user_id et un nonce aleatoire en base64. Mais ce nonce n'est JAMAIS stocke cote serveur ni valide au retour. Le callback decode simplement le state et en extrait credential_id et user_id. Un attaquant peut :
credential_id et user_id de la victimeLe risque est attenue par le fait que le credential_id est un UUID difficile a deviner.
Fix propose : Stocker le nonce en base de donnees ou en session au moment de la generation du state, puis le verifier au callback.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/ssh.ts, ligne ~943
Certitude : CERTAIN
Code :
hostVerifier: (key) => {
return true; // Accept all for now
}
Analyse : La verification de la cle hote SSH est completement desactivee. Cela rend le service vulnerable aux attaques MITM (Man-in-the-Middle). Un attaquant sur le reseau peut intercepter les connexions SSH et capturer les credentials (mots de passe, commandes, resultats).
Impact : En contexte LAN (192.168.1.x), le risque est modere car le reseau est controle. Mais pour les connexions SSH via WireGuard vers des machines externes, le risque est reel.
Fix propose :
hostVerifier: (key) => {
const fingerprint = key.hash('sha256');
// Compare with stored fingerprint from instance metadata
const storedFingerprint = sshConfig.fingerprint;
if (!storedFingerprint) {
// TOFU: Trust On First Use - store and accept
return true;
}
return fingerprint === storedFingerprint;
}
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/ssh.ts, ligne ~997
Certitude : CERTAIN
Code :
const installCommand = `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '${sshPublicKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
Analyse :
La variable sshPublicKey est interpolee directement dans une commande shell entre single quotes. Si la cle publique contient un single quote ('), la commande shell est cassee et du code arbitraire peut etre injecte.
Exemple d'exploitation : une cle publique contenant ' ; rm -rf / ; echo ' produirait :
echo '' ; rm -rf / ; echo '' >> ~/.ssh/authorized_keys
En pratique, les cles SSH generees par le code ne contiennent pas de single quotes (base64 + prefixe standard), mais si sshPublicKey provient d'une entree utilisateur, l'injection est possible.
Fix propose :
const escapedKey = sshPublicKey.replace(/'/g, "'\\''");
const installCommand = `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
Fichier : /stock_8to/33800-stack/projects/connectors-api/conf.prod.gouroubleu.yml, ligne 57
Certitude : CERTAIN
Code :
env:
NODE_TLS_REJECT_UNAUTHORIZED: "0"
Analyse : Cette variable d'environnement desactive la verification des certificats TLS pour TOUTES les requetes HTTPS sortantes du processus. Cela signifie que les appels vers les APIs externes (Google, GitHub, etc.) acceptent des certificats invalides, auto-signes, ou expires.
Le code dans fetch.ts (ligne 171) gere deja les certificats auto-signes pour les services internes de maniere ciblee :
const tlsOptions = url.startsWith('https://192.168.') || url.startsWith('https://10.')
? { tls: { rejectUnauthorized: false } }
: {};
La variable globale est donc redondante avec le mecanisme cible et introduit un risque inutile pour les connexions externes.
Fix propose : Supprimer NODE_TLS_REJECT_UNAUTHORIZED: "0" de conf.prod.gouroubleu.yml. Le mecanisme cible dans fetch.ts suffit pour les services internes.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/ssh.ts, ligne ~105-110
Certitude : CERTAIN
Code :
const ENCRYPTION_KEY = process.env.SSH_ENCRYPTION_KEY || 'default-key-change-in-production-32!';
export function encrypt(text: string): string {
const key = scryptSync(ENCRYPTION_KEY, 'salt', 32);
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', key, iv);
// ...
}
Analyse :
scryptSync est la chaine fixe 'salt'. En consequence, la cle derivee est toujours la meme pour un meme ENCRYPTION_KEY. Cela ne reduit pas la securite du chiffrement directement (l'IV est aleatoire), mais annule l'interet du salt : si la cle maitre fuite, toutes les valeurs chiffrees sont immediatement dechiffrables.'default-key-change-in-production-32!' serait catastrophique s'il etait utilise en prod. Verifie dans conf.prod.gouroubleu.yml : SSH_ENCRYPTION_KEY: "@SSH_ENCRYPTION_KEY" est injecte via secret, donc le fallback n'est PAS utilise en production.Fix propose : Utiliser un sel aleatoire stocke avec le ciphertext (comme l'IV l'est deja).
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/routineExpressions.ts
Certitude : PROBABLE
Code (dans evaluateCondition avec l'operateur matches) :
case 'matches':
return new RegExp(right as string).test(String(left));
Analyse :
L'expression reguliere vient de la configuration de la routine (champ right du noeud condition). Un utilisateur peut creer une routine avec un pattern regex malveillant qui cause un ReDoS (Regular Expression Denial of Service). Exemple : (a+)+$ avec une entree de type aaaaaaaaaaaaaaaaax.
Impact : Blocage du thread d'execution pendant la duree du ReDoS. Attenue par le MAX_EXECUTION_TIME = 300000 (5 min) dans routineExecutor.ts.
Fix propose : Utiliser un timeout sur l'execution regex ou valider les patterns contre des constructions dangereuses.
new RegExp() depuis entree utilisateur dans transformFichier : /stock_8to/33800-stack/projects/connectors-api/src/services/routineExecutor.ts, ligne 653
Certitude : CERTAIN
Code :
case 'replace':
if (typeof data !== 'string') return data;
return data.replace(new RegExp(op.config.pattern, op.config.flags || 'g'), op.config.replacement || '');
Analyse :
Meme probleme de ReDoS que le finding #12. Le pattern provient de la configuration de la routine (entree utilisateur). De plus, le replacement peut contenir des patterns de remplacement speciaux ($1, $&, etc.) mais sans risque de securite au-dela du ReDoS.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/ssh.ts, ligne ~1068
Certitude : PROBABLE
Analyse :
La methode upload() dans ssh.ts utilise un heredoc pour ecrire du contenu sur le serveur distant. Si le contenu contient des sequences speciales du heredoc (ex: EOF sur une ligne seule), la commande sera mal formee. La variable escapedContent est calculee mais il faudrait verifier qu'elle est bien utilisee dans la commande finale.
Impact : Corruption de fichier uploade, ou injection de commande si le contenu est malveillant.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/ssh.ts
Certitude : CERTAIN
Code dans index.ts (ligne 2498-2508) :
if (ssh.isDangerous(body.command)) {
if (!body.force) {
return {
success: false,
warning: true,
message: 'This command appears dangerous. Add "force": true to execute anyway.',
command: body.command
};
}
}
Analyse :
Les commandes dangereuses (rm -rf, mkfs, dd, shutdown, etc.) declenchent un avertissement mais sont executables avec force: true. Le log d'audit enregistre la commande, mais il n'y a pas de blocage hard pour les commandes les plus destructives. C'est un choix de design (pas un bug), mais cela merite attention.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/index.ts, lignes 5324-5354
Certitude : CERTAIN
Code :
.post('/api/webhooks/:routineId', async ({ params, body, query }) => {
// Public endpoint (no auth required) - protected by optional token
// ...
if (expectedToken) {
const providedToken = query.token || (body as any)?.token;
if (providedToken !== expectedToken) {
return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 403 });
}
}
Analyse :
L'endpoint webhook est public par design. La protection par token est optionnelle (if (expectedToken)). Si le webhook est configure sans token, n'importe qui peut declencher l'execution de la routine. Meme avec token, il n'y a pas de protection anti-replay : un meme webhook peut etre rejoue indefiniment.
Fix propose : Rendre le token obligatoire. Ajouter un timestamp + HMAC pour la protection anti-replay.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/index.ts, ligne 4793
Certitude : CERTAIN
Code :
.get('/api/ai/attachments/:id/file/*', async ({ params }) => {
// No auth required — the UUID itself is the secret
Analyse : Les fichiers AI (images generees, etc.) sont accessibles a quiconque possede l'UUID. Les UUIDs v4 sont suffisamment aleatoires (122 bits d'entropie) pour rendre le brute-force impraticable. C'est un pattern "capability URL" acceptable pour du contenu non sensible. Cependant, les URLs sont potentiellement loguees et pourraient etre partagees involontairement.
Impact : Faible. Le contenu genere par l'IA n'est generalement pas confidentiel.
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/index.ts, lignes 1946-2065
Certitude : CERTAIN
Code (lignes 1946-1948) :
.get('/api/audit/logs', async ({ query, request }) => {
try {
const userId = getUserFromRequest(request.headers);
const { logs, total } = await audit.list({
userId: query.user_id || undefined,
Et (ligne 2000) :
.get('/api/audit/logs/:id', async ({ params }) => {
try {
const log = await audit.get(params.id);
Analyse :
getUserFromRequest() est la version SYNCHRONE qui ne valide pas les API keys et ne fait pas de throw si le token est absent - elle retourne simplement null. Le userId de l'appelant n'est pas utilise pour filtrer les logs : le parametre query.user_id permet de filtrer, mais c'est un filtre optionnel fourni par le client.
GET /api/audit/logs/:id n'a AUCUNE verification d'auth. N'importe qui avec acces reseau peut lire n'importe quel log d'audit.
Les logs d'audit peuvent contenir des informations sensibles (IPs, actions effectuees, noms d'instances).
Fix propose : Ajouter requireAuth() et filtrer obligatoirement par userId de l'appelant (sauf pour les admins).
Fichier : /stock_8to/33800-stack/projects/connectors-api/src/services/oauth.ts
Certitude : CERTAIN
Analyse :
Les client_secret des credentials OAuth sont stockes en clair dans la table oauth_credentials de Supabase. Si la base de donnees est compromise, tous les secrets OAuth sont exposes. Ce n'est pas un probleme si la base de donnees est bien protegee, mais c'est une pratique sous-optimale.
Fix propose : Chiffrer les client_secret avec la meme mecanique que les credentials SSH (encrypt()/decrypt()).
Pour etre exhaustif, voici les points qui ont ete verifies et sont conformes :
Verifie : conf.prod.gouroubleu.yml ligne 45 injecte SSH_ENCRYPTION_KEY: "@SSH_ENCRYPTION_KEY" via un secret. Le fallback hardcode ('default-key-change-in-production-32!') n'est PAS utilise en production.
Verifie : src/services/apiKeys.ts utilise SHA-256 pour hasher les cles, ne stocke jamais la cle en clair apres creation, verifie l'expiration et la revocation. L'implementation est solide.
Verifie : Contrairement a wireguard.ts, le fichier vpn-profiles.ts utilise supabase.select(), supabase.insert(), etc. correctement.
Verifie : MAX_NODES = 50, MAX_LOOP_ITERATIONS = 200, MAX_API_CALLS = 30, MAX_EXECUTION_TIME = 300000 (5 min). Ces limites empechent les abus via les routines.
Verifie : fetch.ts ne log pas les credentials, utilise les headers d'auth du connecteur de maniere appropriee, et gere les tokens OAuth avec refresh automatique.
Verifie : L'authentification email/password passe par Supabase GoTrue (/auth/v1/token?grant_type=password). Le serveur ne gere pas les mots de passe directement.
Verifie : Le wrapper supabase.ts utilise des parametres PostgREST (filtres eq, is, etc.) qui sont automatiquement parametrises. Pas d'interpolation directe de valeurs dans les requetes.
Implementer la verification de signature JWT (Finding #1)
Ajouter l'authentification aux endpoints admin (Finding #2)
Ajouter le check auth dans /api/fetch (Finding #3)
Sandboxer les actions Transform (Finding #5)
Supprimer NODE_TLS_REJECT_UNAUTHORIZED: "0" (Finding #10)
Corriger l'echappement dans setupAutoKey (Finding #9)
src/ a ete lu integralement avec l'outil Read (pas de grep partiel)