フリーキーズ | 独学プログラミング

SSRF攻撃とは?初心者でもわかる仕組みと防御テクニックを徹底解説

リンドくん

リンドくん

田邉先生、「SSRF攻撃」って言葉を聞いたんですけど、これって何なんですか?SSRFって略語も難しそうで...

たなべ

たなべ

SSRF攻撃はServer-Side Request Forgeryの略で、日本語では「サーバサイドリクエストフォージェリ」というんだ。
簡単に言うと、Webサーバを踏み台にして、本来アクセスできない場所にリクエストを送らせる攻撃なんだよ。

Webアプリケーションのセキュリティを学ぶ上で、SSRF攻撃は見逃せない重要な脅威の一つです。
最近では、AWSやAzureなどのクラウド環境での被害事例も増加しており、OWASP Top 10にも「Server-Side Request Forgery (SSRF)」として独立した項目で掲載されるようになりました。

この記事では、セキュリティ学習を始めたばかりの方でも理解できるよう、SSRF攻撃の仕組みから具体的な防御方法まで、実践的なコード例を交えながら詳しく解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

SSRF攻撃とは何か?その仕組みを理解しよう

リンドくん

リンドくん

でも先生、普通のサーバってセキュリティ対策されてるんですよね?なんでそんな攻撃が成功しちゃうんですか?

たなべ

たなべ

そこがSSRF攻撃の巧妙なところなんだ。
外部からは直接アクセスできない内部リソースでも、サーバ自身がリクエストする場合は「信頼された接続」として扱われることが多いんだよ。

SSRF攻撃の基本概念

SSRF攻撃は、Webアプリケーションの機能を悪用して、サーバから意図しないリクエストを発生させる攻撃手法です。
通常、Webサーバには以下のような特徴があります。

  • 外部からの直接アクセスが制限された内部ネットワークにアクセスできる
  • ファイアウォールやセキュリティグループの内側に位置している
  • 信頼されたIPアドレスからのリクエストとして扱われる

攻撃者はこの特性を利用して、本来アクセスできないはずの内部リソースにアクセスしようとします。

攻撃シナリオ

典型的なSSRF攻撃のシナリオを見てみましょう。

シナリオ 画像URLを指定できる機能の悪用

多くのWebアプリケーションには、「URLを指定して画像を表示する」機能があります。例えば、プロフィール画像をURL指定でアップロードできる機能などです。

# 脆弱なコード例(Python/Flask)
from flask import Flask, request
import requests

app = Flask(__name__)

@app.route('/fetch-image')
def fetch_image():
    # ユーザーが指定したURLから画像を取得
    image_url = request.args.get('url')
    
    # 危険!URLの検証なしでリクエストを送信
    response = requests.get(image_url)
    
    return response.content

このコードでは、ユーザーが指定したURLに対して何の検証もせずにリクエストを送っています。
攻撃者は以下のようなURLを指定できてしまいます。

https://____.com/fetch-image?url=http://localhost:8080/admin
https://____.com/fetch-image?url=http://100.0.0.0/latest/meta-data/

この例では内部の管理画面とメタデータエンドポイントにアクセスしようとしています。

なぜSSRF攻撃が危険なのか

SSRF攻撃が成功すると、以下のような被害が発生する可能性があります。

  • 内部ネットワークのスキャン → 外部からは見えない内部サーバの存在や構成を調査される
  • 機密情報の漏洩 → 内部APIやデータベース、設定ファイルなどにアクセスされる
  • クラウドメタデータの窃取 → AWSやAzureの認証情報が盗まれる
  • 内部サービスの悪用 → 内部システムの脆弱性を利用した二次攻撃が行われる

特にクラウド環境では、メタデータエンドポイントから認証情報が取得できるため、深刻な被害につながることがあります。

SSRF攻撃の実際の手口と攻撃パターン

リンドくん

リンドくん

攻撃者はどうやって内部のURLを知るんですか?

たなべ

たなべ

よく使われるURLパターン標準的な構成を予測して試すことが多いんだ。
例えばlocalhost127.0.0.1、クラウドのメタデータエンドポイントなんかは定番だね。

典型的な攻撃パターン

SSRF攻撃には、いくつかの典型的なパターンがあります。

パターン① 内部サービスへのアクセス

# 内部管理画面へのアクセス試行
http://localhost/admin
http://127.0.0.1:8080/api/admin

# 内部データベースへのアクセス試行
http://internal-db.local:3306
http://192.168.1.10:27017

パターン② クラウドメタデータの窃取

AWSの場合、以下のようなエンドポイントが狙われます。

# AWSメタデータエンドポイント
http://100.0.0.0/latest/meta-data/
http://100.0.0.0/latest/meta-data/iam/security-credentials/

# Azureメタデータエンドポイント
http://100.0.0.0/metadata/instance?api-version=2021-02-01

これらのエンドポイントから、一時的なアクセスキーやIAMロールの認証情報が取得できてしまいます。

パターン③ プロトコルの悪用

攻撃者はHTTP/HTTPS以外のプロトコルを使用することもあります。

# ファイルシステムへのアクセス
file:///etc/passwd
file:///c:/windows/system32/drivers/etc/hosts

# 内部サービスへの接続
gopher://internal-service:9000/_POST%20...
dict://localhost:11211/stats

URLエンコードやリダイレクトを使った回避テクニック

攻撃者は、基本的なフィルタリングを回避するために、さまざまなテクニックを使用します。

# URLエンコードによる回避
http://____.com?url=http%3A%2F%2Flocalhost%2Fadmin

# 2進数表記による回避
http://127.0.0.1 → http://2130706433

# 16進数表記による回避
http://127.0.0.1 → http://0x7f000001

# リダイレクトを使った回避
攻撃者が用意したURL → 内部URLへリダイレクト

これらの手法により、単純なブラックリスト方式のフィルタリングは簡単に突破されてしまいます。

実際の攻撃コード例

Node.jsで書かれた脆弱なコードと、それを利用した攻撃の流れを見てみましょう。

// 脆弱なコード例(Node.js/Express)
const express = require('express');
const axios = require('axios');
const app = express();

app.get('/proxy', async (req, res) => {
    const targetUrl = req.query.url;
    
    try {
        // 危険!URLの検証なしでリクエスト
        const response = await axios.get(targetUrl);
        res.send(response.data);
    } catch (error) {
        res.status(500).send('Error fetching URL');
    }
});

app.listen(3000);

攻撃者はこのエンドポイントを以下のように悪用できます。

# 内部管理画面へのアクセス
curl "http://____.com/proxy?url=http://localhost:8080/admin/users"

# AWSメタデータの窃取
curl "http://____.com/proxy?url=http://100.0.0.0/latest/meta-data/iam/security-credentials/"

このように、ユーザー入力を適切に検証せずに外部リクエストを行う機能は、SSRF攻撃の入り口になってしまうのです。

SSRF攻撃を防ぐための具体的な対策テクニック

リンドくん

リンドくん

じゃあ、どうやって防げばいいんですか?URLをチェックすればいいんですよね?

たなべ

たなべ

そうだね、でも単純なチェックだけじゃ不十分なんだ。多層防御の考え方で、複数の対策を組み合わせることが重要なんだよ。具体的に見ていこうか。

対策① ホワイトリスト方式のURL検証

最も基本的で効果的な対策は、許可するドメインやIPアドレスをホワイトリストで管理することです。

# セキュアなコード例(Python)
from urllib.parse import urlparse
import ipaddress
import requests

# 許可するドメインのリスト
ALLOWED_DOMAINS = ['trusted-api.____.com', 'cdn.____.com']

def is_safe_url(url):
    """URLが安全かチェックする"""
    try:
        parsed = urlparse(url)
        
        # HTTPSのみ許可
        if parsed.scheme != 'https':
            return False
        
        # ホワイトリストに含まれるドメインのみ許可
        if parsed.hostname not in ALLOWED_DOMAINS:
            return False
        
        return True
    except:
        return False

@app.route('/fetch-image')
def fetch_image():
    image_url = request.args.get('url')
    
    # URL検証
    if not is_safe_url(image_url):
        return "Invalid URL", 400
    
    response = requests.get(image_url, timeout=5)
    return response.content

対策② 内部IPアドレスへのアクセス禁止

内部ネットワークやローカルホストへのアクセスを明示的に禁止します。

def is_internal_ip(hostname):
    """内部IPアドレスかチェックする"""
    try:
        ip = ipaddress.ip_address(hostname)
        
        # プライベートIPアドレスをチェック
        if ip.is_private:
            return True
        
        # ループバックアドレスをチェック
        if ip.is_loopback:
            return True
        
        # リンクローカルアドレスをチェック
        if ip.is_link_local:
            return True
        
        # AWSメタデータエンドポイント
        if str(ip) == '100.0.0.0':
            return True
        
        return False
    except:
        return False

def is_safe_url_with_ip_check(url):
    """URLとIPアドレスの両方をチェック"""
    try:
        parsed = urlparse(url)
        
        # スキームチェック
        if parsed.scheme not in ['http', 'https']:
            return False
        
        # ホスト名の解決
        hostname = parsed.hostname
        if not hostname:
            return False
        
        # 内部IPアドレスのチェック
        if is_internal_ip(hostname):
            return False
        
        # DNSで解決されたIPアドレスもチェック
        import socket
        try:
            resolved_ip = socket.gethostbyname(hostname)
            if is_internal_ip(resolved_ip):
                return False
        except:
            return False
        
        return True
    except:
        return False

対策③ ネットワークレベルでの分離

アプリケーションレベルの対策に加えて、ネットワーク構成での防御も重要です。

# Docker Composeでのネットワーク分離例
version: '3'
services:
  web:
    image: myapp:latest
    networks:
      - external
    # 内部ネットワークへのアクセスを制限
    
  database:
    image: postgres:14
    networks:
      - internal
    # 外部からのアクセスを完全に遮断
    
networks:
  external:
    driver: bridge
  internal:
    driver: bridge
    internal: true  # 外部通信を禁止

対策④ リダイレクトの制限

HTTPリダイレクトを悪用した攻撃を防ぐため、リダイレクトを制限します。

// Node.jsでのセキュアな実装例
const axios = require('axios');

async function safeFetch(url) {
    // URLの事前検証
    if (!isAllowedUrl(url)) {
        throw new Error('URL not allowed');
    }
    
    try {
        const response = await axios.get(url, {
            maxRedirects: 0,  // リダイレクトを禁止
            timeout: 5000,    // タイムアウト設定
            validateStatus: (status) => status === 200  // 200のみ許可
        });
        
        return response.data;
    } catch (error) {
        if (error.response && error.response.status === 302) {
            // リダイレクトが検出された場合
            throw new Error('Redirects are not allowed');
        }
        throw error;
    }
}

対策⑤ メタデータエンドポイントの保護

クラウド環境では、メタデータエンドポイントへのアクセスを制限する追加の対策が必要です。

# メタデータエンドポイントを明示的にブロック
BLOCKED_HOSTS = [
    '100.0.0.0',  # AWS/Azure メタデータ
    'metadata.google.internal',  # GCP メタデータ
    'fd00:ec2::254',  # AWS IPv6 メタデータ
]

def is_blocked_host(hostname):
    """ブロックすべきホストかチェック"""
    if hostname in BLOCKED_HOSTS:
        return True
    
    # IPアドレスとしてパース
    try:
        ip = ipaddress.ip_address(hostname)
        if str(ip) in BLOCKED_HOSTS:
            return True
    except:
        pass
    
    return False

対策⑥ 最小権限の原則

Webアプリケーションの実行ユーザーには、必要最小限の権限のみを付与します。

# Dockerfileでの実装例
FROM python:3.9-slim

# 非rootユーザーを作成
RUN useradd -m -u 1000 appuser

# アプリケーションファイルをコピー
COPY --chown=appuser:appuser . /app
WORKDIR /app

# 非rootユーザーとして実行
USER appuser

CMD ["python", "app.py"]

これらの対策を組み合わせることで、多層防御を実現し、SSRF攻撃のリスクを大幅に低減できます。

まとめ

リンドくん

リンドくん

なるほど!SSRF攻撃って、単純に見えて実は奥が深いんですね。

たなべ

たなべ

その通り!セキュリティは常に攻撃者の視点で考えることが大切なんだ。
ユーザー入力を信頼せず、複数の防御層を設けることで、安全なアプリケーションを作れるようになるよ。

今回は、SSRF攻撃の仕組みから具体的な防御テクニックまで、初心者の方でも実践できる内容を中心に解説してきました。
最後に、重要なポイントを整理しておきましょう。

  • サーバを踏み台にする攻撃 → 外部からは見えない内部リソースにアクセスされるリスク
  • ユーザー入力のURL処理が狙われる → 画像取得、Webhook、プロキシ機能などが攻撃対象
  • クラウド環境では特に危険 → メタデータエンドポイントから認証情報が窃取される可能性
  • 単純なブラックリストは不十分 → エンコードやリダイレクトで回避されてしまう
  • ホワイトリスト方式の採用 → 許可するドメインやIPを明示的に管理
  • 内部IPアドレスのブロック → プライベートIPやループバックアドレスへのアクセス禁止
  • ネットワークレベルの分離 → アプリケーションサーバと内部サービスの分離
  • リダイレクトの制限 → 予期しないリダイレクトを検出して拒否
  • 最小権限の原則 → アプリケーションに必要最小限の権限のみ付与

セキュリティは一朝一夕で身につくものではありませんが、基本を理解し、実践を積み重ねることで確実にスキルアップできます。
この記事で学んだ内容を、ぜひご自身のプロジェクトに適用してみてください。

セキュリティは、すべての開発者が意識すべき重要なスキルです。
SSRF攻撃の防御を通じて、セキュアな設計とコーディングの基礎を身につけ、信頼されるエンジニアへと成長していきましょう!

この記事をシェア

関連するコンテンツ