flawopen.com/ssrf/Javascript
How axios and fetch allow internal network scanning and cloud metadata theft, and how to implement safe lookup agents in Node.js.
Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.
Web Application SecurityCWE-918 betroffen ist.CWE-918Defense-in-DepthDie Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.
Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).
Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.
Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.
// 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');
}
});
// 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);
}
});