SSRF از طریق ویژگی «پیشنمایش لینک» برای خواندن متادیتای سرویسهای ابری
فیچر «آدرس عکس پروفایل را وارد کن، ما دانلودش میکنیم» بدون فیلتر، سرور را وادار میکند به آدرسهای داخلی/متادیتای cloud درخواست بزند.
بسیاری از سایتها یک فیچر «آدرس عکس را بده، ما دانلودش میکنیم و نمایش میدهیم» دارند. نمونهی کد (Node.js/Express):
// نسخهی آسیبپذیر
app.post('/profile/avatar-from-url', async (req, res) => {
const { url } = req.body;
const response = await fetch(url); // <- هیچ فیلتری روی url نیست
const buffer = await response.arrayBuffer();
saveAvatar(req.user.id, buffer);
res.json({ ok: true });
});
شناسایی
بهجای یک آدرس عکس واقعی، یک آدرس داخلی میفرستیم:
POST /profile/avatar-from-url
{ "url": "http://127.0.0.1:6379/" } // بررسی سرویسهای داخلی سرور (مثل redis)
اگر پاسخ یا زمان پاسخ تفاوت معناداری با یک آدرس نامعتبر داشته باشد، یعنی سرور واقعاً به آن آدرس داخلی وصل شده (SSRF تایید شد).
بهرهبرداری جدیتر: متادیتای Cloud
در AWS/GCP/Azure یک endpoint داخلی ویژه برای گرفتن credential موقت ماشین وجود دارد که فقط از خودِ سرور قابل دسترسی است:
POST /profile/avatar-from-url
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
اگر سرور این مسیر را واکشی کند و نتیجه (حتی بهصورت غیرمستقیم، مثلاً در پیام خطا) برگردانده شود، مهاجم میتواند credential موقت IAM را به دست بیاورد — یعنی دسترسی به سرویسهای ابری پشت سرور، نه فقط خودِ وباپ.
راهحل
- Allowlist سختگیرانه روی دامنه/پروتکل (فقط https، فقط دامنههای شناختهشدهی CDN تصویر).
- Resolve کردن DNS قبل از درخواست و رد کردن IPهای private/loopback/link-local (۱۰.x، ۱۹۲.۱۶۸.x، ۱۲۷.x، ۱۶۹.۲۵۴.x).
- در محیط ابری، غیرفعال یا نسخهی IMDSv2-only کردن متادیتای instance (نیاز به token، قابل SSRF ساده نیست).
// نمونهی رفع با allowlist + چک IP
const { URL } = require('url');
const dns = require('dns').promises;
async function isSafeUrl(rawUrl) {
const u = new URL(rawUrl);
if (u.protocol !== 'https:') return false;
const { address } = await dns.lookup(u.hostname);
const blocked = /^(127\.|10\.|172\.(1[6-9]|2\d|3[0-1])\.|192\.168\.|169\.254\.)/;
return !blocked.test(address);
}