Date : 15/02/2026
Status : IMPLEMENTEE
Projet : /stock_8to/33800-stack/projects/connectors-api/
Runtime : Bun / Elysia (TypeScript)
Fichiers audites : 35 fichiers source TypeScript
| Severite | Nombre |
|---|---|
| CRITIQUE | 5 |
| HAUTE | 10 |
| MOYENNE | 10 |
| BASSE | 9 |
| TOTAL | 34 |
src/middleware/jwt.ts ligne 29-51verifyJWT() decode le payload JWT (base64) mais ne verifie JAMAIS la signature HMAC. Un attaquant peut forger un JWT arbitraire avec n'importe quel sub (user_id). Le JWT_SECRET est lu ligne 8 mais jamais utilise.crypto.createHmac('sha256', JWT_SECRET) sur header.payload et comparer avec la signature (parts[2]).src/index.ts ligne 1767-1787POST /api/connectors/:id/token permet de definir un token systeme pour un connecteur. Aucun middleware d'authentification n'est applique. Le handler ne verifie ni JWT ni API key.requireAuthWithScope(request.headers, SCOPES.CONNECTORS_WRITE) et verifier que l'utilisateur est admin.src/index.ts lignes 201-204/api/fetch utilise getAuthFromRequest() qui retourne null si pas de token. Le code continue avec userId = null, permettant des appels proxy sans authentification.userId: null.if (!auth) return { success: false, status: 401, error: 'Authentication required' }; apres la ligne 203.src/services/ssh.ts ligne 997sshPublicKey est interpolee directement dans une commande shell avec des single quotes. Si la cle contient un single quote (ou est manipulee), la commande shell est injectable : echo '${sshPublicKey}' >> ~/.ssh/authorized_keysecho, ou au minimum echapper correctement la cle.src/services/ssh.ts lignes 1067-1068escapedContent est calculee (single quotes echappees) mais c'est fileContent (non echappe) qui est utilise dans le heredoc. Si le contenu du fichier contient la chaine EOFCONNECTORSHUB, le heredoc se termine prematurement et le reste est interprete comme commande shell.src/services/ssh.ts ligne 105, src/services/wireguard.ts ligne 16, src/services/vpn-profiles.ts ligne 12ENCRYPTION_KEY = process.env.SSH_ENCRYPTION_KEY || 'default-key-change-in-production-32!'. Si la variable d'environnement n'est pas definie, tous les credentials SSH/VPN sont chiffres avec une cle publiquement connue (dans le code source).SSH_ENCRYPTION_KEY n'est pas defini. Ne JAMAIS avoir de valeur par defaut pour une cle de chiffrement.src/services/ssh.ts lignes 109, 119 ; src/services/wireguard.ts lignes 78, 88 ; src/services/vpn-profiles.ts lignes 78, 88crypto.scryptSync(ENCRYPTION_KEY, 'salt', 32) utilise le salt litteral 'salt' dans les 3 fichiers (6 occurrences). Le salt est cense etre unique et aleatoire par operation.src/services/actions.ts ligne 853new Function('input', 'ctx', ...) execute du JavaScript fourni par l'utilisateur (config.code). Le contexte ctx expose fetch() (appels API), la liste des instances, actions et routines.ctx.fetch().isolated-vm.src/services/auth.ts ligne 26, src/services/oauth.ts lignes 99-103state OAuth contient user_id et nonce en base64, mais n'est pas signe (HMAC). Un attaquant peut forger un state avec n'importe quel user_id et rediriger le callback OAuth pour lier son propre compte Google au compte de la victime.src/index.ts ligne 53allowedHeaders: ['Content-Type', 'Authorization'] mais le middleware JWT (ligne 61 de jwt.ts) lit aussi headers.get('x-api-key'). Les requetes CORS preflight avec X-API-Key seront rejetees par le navigateur.'X-API-Key' a la liste allowedHeaders.src/services/wireguard.ts lignes 470-486// Filter tunnels by user (would need to track this) mais le code retourne tous les tunnels du sidecar sans filtrage.src/services/wireguard.ts lignes 153-158, 173-177, 233-238, 271-275, 317-322, 371-375, 434-439, 517-520, 543-546, 592-597, 626-631, 640-644supabase.from('user_connectors').select(...), .update(...), .eq(...), .single(), .not(...). Mais le wrapper Supabase (src/db/supabase.ts) n'expose PAS ces methodes chainees. Il expose supabase.select(), supabase.update(), supabase.insert(). L'API chaineee est celle du SDK @supabase/supabase-js qui n'est PAS installe (absent de package.json).supabase.select(), supabase.update() etc. conforme a src/db/supabase.ts.src/index.ts lignes 207-208const requiredScope = body.method === 'GET' ? SCOPES.FETCH : SCOPES.FETCH; - les deux branches du ternaire retournent SCOPES.FETCH. La distinction GET vs autres methodes est morte.FETCH_READ n'est jamais utilise.const requiredScope = body.method === 'GET' ? SCOPES.FETCH_READ : SCOPES.FETCH;src/index.ts ligne 1648/api/user/connectors/ utilise auth.getAuthUrl(connector, userId) ou auth est le service importe de ./services/auth. Mais dans le scope parent, auth est aussi le nom de la variable AuthResult issue de requireAuthWithScope(). Si la variable auth du scope englobant masque l'import, l'appel .getAuthUrl() echoue.auth en authService ou la variable locale auth en authResult pour eviter le shadowing.src/index.ts lignes 1352-1359, 1405-1412, 1448-1455user_id, connector_id, resource_id (snake_case). L'interface AuditLogInput attend userId, connectorId, resourceId (camelCase). De plus, audit.log(input, request) passe un second argument request que la signature log(input: AuditLogInput) n'accepte pas (ignore silencieusement).user_id, connector_id, resource_id sont ignores. Les audit logs de creation/update/suppression d'endpoints n'ont pas de user_id ni de connector_id.userId, connectorId, resourceId (camelCase). Supprimer le second argument request.src/services/routineExpressions.ts lignes 117-119case 'equals': return left == right; et case 'not_equals': return left != right; utilisent la comparaison lache JavaScript. "0" == false est true, "" == 0 est true, etc.=== / !== apres conversion de type appropriee.src/services/ssh.ts lignes 1033-1043This is a placeholder that won't work with all keys. La conversion PEM SPKI -> OpenSSH format est simplifiee a ssh-rsa ${base64} ce qui ne correspond pas au format reel (le format OpenSSH a un header binaire specifique).sshpk ou generer directement des cles ed25519 au format OpenSSH natif.src/services/actions.ts lignes 191-214query (string) et params (URLSearchParams) sont construites avec la logique OR PostgREST et les filtres, mais ne sont JAMAIS utilisees. La requete reelle (ligne 232-235) utilise l'objet eq et le wrapper supabase.select().include_public (OR logic) ne fonctionne pas. Seules les actions du user sont retournees, jamais les publiques.src/services/ai.ts ligne 159let baseUrl = 'http://192.168.1.12:5501' comme fallback pour downloadJobFile.process.env.AI_ORCHESTRATOR_URL ou resoudre via l'instance connectors.src/services/notify.ts ligne 5const NOTIF_URL = process.env.NOTIF_URL || 'http://192.168.1.12:5300'NOTIF_URL est toujours defini dans l'env. Lever une erreur si absent.src/services/userStorage.ts ligne 501const SUPABASE_URL = process.env.SUPABASE_URL || 'http://192.168.1.12:8000'src/db/supabase.ts qui a deja la meme logique.src/services/aiChat.ts lignes 587-590, src/services/intentExecutor.ts lignes 215-218const GPU_NAMES: Record<number, string> = { 0: 'RTX 3090', 1: 'RTX 2070 Super' } est defini identiquement dans 2 fichiers.src/constants/gpu.ts ou src/services/ai.ts).package.json ligne 3 (2.3.2), src/index.ts ligne 60 (2.5.0), src/index.ts ligne 181 (2.4.0)import pkg from '../package.json'; const VERSION = pkg.version;src/index.ts lignes 207-208if (auth?.scopes !== null && auth?.scopes) est vrai UNIQUEMENT pour les API keys (scopes = array). Pour les JWT (scopes = null), le check est saute. Mais le vrai probleme est que si auth est null (pas authentifie), auth?.scopes est undefined, et undefined !== null est true, donc le code entre dans le bloc de verification des scopes sur un objet null.auth?.scopes est undefined, undefined !== null est true, undefined est falsy, donc le && court-circuite et le check est saute. Le code continue sans authentification. Le comportement est correct par accident mais fragile.src/services/audit.ts (lignes de la fonction search)search() charge jusqu'a 1000 lignes d'audit_logs puis filtre en JavaScript (data.filter(...)). Avec le temps, le volume d'audit logs augmentera.eq, gte, lte, ilike.src/index.ts (section WebSocket SSH shell)Client ssh2 et un stream shell.src/index.ts (section WebSocket SSH shell, sshSessions Map)src/services/auth.ts lignes 92-106setToken() accepte un connector et un userId optionnel. Quand userId est absent (token systeme), n'importe qui avec acces a l'endpoint (cf. S-02) peut ecraser le token.src/services/intentDetector.ts lignes 118, et plusieurs autres dans le meme fichierconsole.log('[INTENT] normalized:', JSON.stringify(normalized)); et d'autres logs de debug sont laisses dans le code de production.process.env.DEBUG.src/services/actions.ts lignes 191-214query et params construites mais jamais utilisees.src/services/ssh.ts, src/services/wireguard.ts, src/services/vpn-profiles.tsencrypt() et decrypt() sont copiees-collees identiquement dans 3 fichiers. Meme cle, meme salt, meme algorithme.src/utils/encryption.ts avec un export unique.src/services/audit.ts lignes 14-47 vs usages dans index.tsAuditAction ne liste pas 'vpn.generate_keys', 'vpn.configure', 'vpn.connect', 'vpn.disconnect', 'vpn.delete_config', 'ssh.create_instance', 'ssh.shell_input', 'routine.execute.webhook', 'endpoint.create'. Le type accepte AuditAction | string (ligne 67) donc pas d'erreur TypeScript, mais le typage perd sa valeur.AuditAction.15-34. Tous les MOYENNE et BASSE
Cet audit est en LECTURE SEULE. Aucun fichier n'a ete modifie. Les numeros de ligne peuvent evoluer si des modifications sont faites entre-temps. Chaque probleme a ete verifie directement dans le code source.