flawopen.com/ssrf/Javascript

● CWE-918 · Haute
Recherche · FlawOpen

Server-Side Request Forgery in JavaScript

How axios and fetch allow internal network scanning and cloud metadata theft, and how to implement safe lookup agents in Node.js.

💡 Explication en Langage Simple (ELI5)

Imaginez envoyer un coursier chercher un colis dans une boutique publique, mais en lui donnant l'adresse du coffre-fort du bureau de la direction. Comme le coursier possède un badge d'accès interne, il ouvre le coffre et vous remet les secrets de l'entreprise.

Concepts Clés et Termes

Web Application Security
Composant d'architecture clé affecté par CWE-918.
CWE-918
Classification standard Common Weakness Enumeration (CWE) pour ssrf-javascript.
Defense-in-Depth
Vérification d'ingénierie multicouche et isolation des limites à l'exécution.

Déroulement de l'Attaque Étape par Étape

Step 1

Réception d'une URL Distante Non Fiable

L'application prend en charge une URL externe pour récupérer un avatar, un webhook ou convertir un document.

Step 2

Adressage de Réseau Interne

L'attaquant fournit une URL ciblant les métadonnées cloud (ex. http://169.254.169.254/latest/meta-data/) ou une IP privée de bouclage.

Step 3

Émission de Socket Non Filtrée

Le client HTTP du serveur émet une requête depuis le réseau privé sans résoudre au préalable l'adresse IP cible.

Step 4

Exfiltration des Identifiants Cloud

Le service de métadonnées interne fait confiance au serveur et renvoie les clés IAM temporaires ou les jetons d'accès.

Code Source : Vulnérable vs Sécurisé

✕ IMPLÉMENTATION VULNÉRABLE
// VULNERABLE: Naively fetching user URL with axios
const express = require('express');
const axios = require('axios');

const app = express();

app.get('/proxy-image', async (req, res) => {
  const imageUrl = req.query.url;
  // Attacker input: http://169.254.169.254/latest/meta-data/
  try {
    const response = await axios.get(imageUrl);
    res.send(response.data);
  } catch (err) {
    res.status(500).send('Fetch error');
  }
});
✓ PATCH SÉCURISÉ ET ROBUSTE
// HARDENED: Custom DNS lookup agent blocking private & metadata IP ranges
const express = require('express');
const http = require('http');
const https = require('https');
const dns = require('dns');
const ipaddr = require('ipaddr.js'); // npm i ipaddr.js
const axios = require('axios');

const app = express();

function safeLookup(hostname, options, callback) {
  dns.lookup(hostname, options, (err, address, family) => {
    if (err) return callback(err);

    try {
      const addr = ipaddr.parse(address);
      const range = addr.range();

      // Deny private, loopback, link-local, and reserved ranges
      const blockedRanges = ['loopback', 'private', 'linkLocal', 'carrierGradeNat', 'reserved'];
      if (blockedRanges.includes(range)) {
        return callback(new Error(`SSRF Blocked: Destination IP ${address} is in blocked range (${range})`));
      }

      callback(null, address, family);
    } catch (parseErr) {
      callback(new Error('Invalid IP address resolved'));
    }
  });
}

const safeHttpAgent = new http.Agent({ lookup: safeLookup });
const safeHttpsAgent = new https.Agent({ lookup: safeLookup });

app.get('/proxy-image', async (req, res) => {
  const imageUrl = req.query.url;
  try {
    const parsed = new URL(imageUrl);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return res.status(400).send('Invalid protocol');
    }

    const response = await axios.get(imageUrl, {
      httpAgent: safeHttpAgent,
      httpsAgent: safeHttpsAgent,
      maxRedirects: 0, // Prevent redirect bypasses
      timeout: 3000
    });

    res.send(response.data);
  } catch (err) {
    res.status(403).send('Request blocked: ' + err.message);
  }
});

Liste de Contrôle de Sécurité pour l'Ingénierie

References