flawopen.com/ssrf/Javascript

● CWE-918 · 高
セキュリティ研究 · 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.

💡 わかりやすい解説 (ELI5)

オフィスの宅配便受取係に小包を取りに行くよう依頼する際、外部の店舗ではなく役員室の金庫の部屋番号を指定する場面を想像してください。受取係は社内通行バッジを持っているため、疑われることなく金庫室に入り機密書類を持ち出してしまいます。

主要な概念と専門用語

Web Application Security
主要概念 (Web Application Security):Core architecture component affected by CWE-918.
CWE-918
主要概念 (CWE-918):Standard Common Weakness Enumeration classification for ssrf-javascript.
Defense-in-Depth
主要概念 (Defense-in-Depth):Multi-layered engineering verification and runtime boundary isolation.

ステップ・バイ・ステップの攻撃フロー

Step 1

外部URLパラメータの受信

アプリケーションがユーザーから指定されたURLを受け取り、画像の取得やWebhook配信を実行します。

Step 2

内部ネットワークアドレスの指定

攻撃者がクラウドメタデータサービス(例: http://169.254.169.254/latest/meta-data/)や内部ループバックURLを指定します。

Step 3

保護領域からの制限なきソケット送信

サーバーのHTTPクライアントがプライベートVPC内からリクエストを発行し、内部IPチェックを行いません。

Step 4

クラウド認証情報およびトークンの窃取

メタデータサービスがリクエストを信頼し、一時的なIAM認証情報や内部APIデータを攻撃者に返却します。

ソースコード比較:脆弱 vs 堅牢化

✕ 脆弱な実装
// 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);
  }
});

エンジニアリング&システム堅牢化チェックリスト

References