OWASP Top 10 2021: Server-Side Request Forgery (SSRF) の解説と対策

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

OWASP Top 10 2021: Server-Side Request Forgery (SSRF) の解説と対策

Server-Side Request Forgery(SSRF)は、OWASP Top 10 2021で新たにA10として追加された重要な脆弱性です [1]。この脆弱性は、Webアプリケーションがユーザーから提供されたURLを使用して、外部リソースへのリクエストをサーバー側で生成・実行する際に発生します。攻撃者はこの機能を利用して、サーバーが本来アクセスすべきではない内部ネットワークのリソースや、クラウドプロバイダーのメタデータサービスなどへのリクエストを強制できます。これにより、機密情報の漏洩、内部システムのポートスキャン、あるいは他の内部システムへの攻撃の足がかりとなる可能性があります。

脅威モデル

SSRFの主な脅威モデルは以下の通りです [1, 6]。

  • 内部ネットワークスキャンとアクセス: サーバーがDMZや内部ネットワークにアクセスできる場合、攻撃者はSSRFを利用して内部ネットワークのポートスキャンを実行したり、他の内部Webアプリケーションやサービス(例: 管理コンソール、データベース)へアクセスしたりできます。

  • クラウドメタデータサービスからの情報漏洩: AWS IMDSv1、GCP Metadata Service、Azure Instance Metadata Serviceといったクラウドプロバイダーのメタデータサービスは、一時的な認証情報、APIキー、インスタンス設定など、機密情報を提供します。SSRFはこのメタデータサービスへのアクセスを悪用し、これらの機密情報を窃取する可能性があります [3, 4]。

  • ローカルファイルシステムの読み取り: file:// スキームなどのプロトコルを利用して、サーバーのファイルシステム内のファイルを読み取ることが可能です(例: /etc/passwd, ログファイル、設定ファイル) [2]。

  • 他のプロトコルを用いた攻撃: gopher://dict:// といったURLスキームを悪用し、RedisやMemcachedなどの内部サービスへのコマンド実行、あるいはSMTPサーバーへのメール送信などの攻撃に利用されることがあります [2]。

攻撃シナリオ

SSRFの攻撃シナリオは多岐にわたりますが、ここでは代表的なものを紹介します [2, 3]。

graph TD
    A["攻撃者"] -->|1. 悪意あるURLをWebアプリに送信
(例: callback URL, image URL)| B("Webアプリケーション
(URLフェッチ機能")) B -->|2. URLをパースし、バックエンドリクエスト生成| C{"脆弱性のあるHTTPクライアント/バックエンド処理"} C -->|3. 内部ネットワークへのリクエスト実行
(例: http://169.254.169.254/latest/meta-data/)| D("ターゲットサーバー内部
ネットワーク/サービス") D -->|4. 機密情報応答
(例: AWS IMDSv1認証情報, Redis INFO)| E["クラウドメタデータサービス/内部API/ファイルシステム"] E -->|5. 応答をWebアプリに返却| C C -->|6. 応答内容を攻撃者に転送| A A -->|7. 内部情報漏洩/システム操作| F["攻撃成功"]
  1. クラウドメタデータからの認証情報窃取:

    • 攻撃者は、画像取得機能やPDF生成機能など、外部URLを処理するWebアプリケーションのパラメータに、クラウドメタデータサービスのエンドポイントを指定します。

    • 例: GET /image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/admin

    • 脆弱なアプリケーションは、このURLにリクエストを送信し、取得した一時的なIAM認証情報を攻撃者へ返してしまいます。

  2. 内部システムのポートスキャン/アクセス:

    • 攻撃者は、GET /data?url=http://192.168.1.1:22 のように内部IPアドレスとポートを指定します。

    • アプリケーションがこのURLにリクエストを試み、接続の成功/失敗や応答内容から、内部ネットワークのポート状態を推測できます。これにより、内部に存在する脆弱なサービスを特定し、さらなる攻撃の足がかりとします。

  3. ローカルファイル読み取り:

    • 攻撃者は、GET /report?url=file:///etc/passwd のように file:// スキームを利用してローカルファイルの読み取りを試みます。

    • サーバーがUNIX系OSの場合、/etc/passwd の内容が攻撃者に開示され、システムユーザーの情報が漏洩します。

検出と緩和策

SSRFの検出と緩和には、多層的なアプローチが必要です。

検出

  • ログの監視: アプリケーションログ、Webサーバーログ、ファイアウォールログなどを監視し、異常なリクエストパターン(例: 内部IPアドレスへのアクセス試行、予期しないプロトコルの使用)を検出します [1, 6]。

  • WAF/IPSの導入: Web Application Firewall (WAF) や Intrusion Prevention System (IPS) を導入し、既知のSSRFパターンや内部IPアドレスへのアクセスをブロックするルールを設定します [3]。

緩和策

SSRFの最も効果的な緩和策は、入力検証とネットワークレベルでの制御です [5]。

  1. 入力検証の強化(ホワイトリスト方式):

    • スキームの制限: http:// または https:// のみ許可し、file://gopher://dict:// などの危険なスキームをブロックします。

    • ホストの制限: 許可されたドメイン名またはIPアドレスのホワイトリストを作成し、それ以外のホストへのアクセスを拒否します。特に、127.0.0.10.0.0.0、プライベートIPアドレス範囲(RFC 1918)、およびクラウドメタデータサービスのアドレス(例: 169.254.169.254)へのアクセスを厳格にブロックします。

    • ポートの制限: 許可されたポート番号のみを許可します(例: HTTP/HTTPSのデフォルトポート 80, 443)。

    • URLパーサーの利用: URLを解析する際は、正規表現ではなく、Pythonの urllib.parse やJavaの java.net.URL のような信頼できるURLパーサーを使用します。これにより、URLエンコード、ダブルエンコード、その他の複雑な形式のバイパスを防ぎます [5]。

    誤用例(Python: 検証なしでURLフェッチ)

    import requests
    import sys
    
    # 前提: `url`パラメータは攻撃者が制御可能
    
    def vulnerable_fetch_url(url: str):
        """
        URLの検証をせずにリクエストを実行する脆弱な関数。
        入力: url (str) - 取得するURL
        出力: なし (標準出力に結果を出力)
        計算量: O(ネットワークレイテンシ)
        メモリ: O(応答サイズ)
        """
        print(f"Fetching (vulnerable): {url}")
        try:
    
            # 外部からの入力URLを直接使用。
    
    
            # 攻撃者はここを file:///etc/passwd や http://169.254.169.254/... に変更可能。
    
            response = requests.get(url, timeout=5) 
            print(f"Status: {response.status_code}")
            print(f"Content (first 100 chars): {response.text[:100]}")
        except requests.exceptions.RequestException as e:
            print(f"Error: {e}")
    
    if __name__ == "__main__":
    
        # 実行例: python vulnerable.py http://169.254.169.254/latest/meta-data/iam/security-credentials/
    
    
        # 実行例: python vulnerable.py file:///etc/passwd
    
        if len(sys.argv) > 1:
            vulnerable_fetch_url(sys.argv[1])
        else:
            print("Usage: python vulnerable.py <url>")
    

    安全な代替策(Python: URL検証と制限)

    import requests
    import sys
    from urllib.parse import urlparse
    import ipaddress
    import socket
    
    def safe_fetch_url(url: str):
        """
        SSRF対策を施したURLフェッチ関数。
    
        - スキームのホワイトリスト: http/httpsのみ許可
    
        - ホストのブラックリスト: 内部IPアドレスや予約IPアドレスへのアクセスを禁止
    
        - リダイレクトの制限
        入力: url (str) - 取得するURL
        出力: なし (標準出力に結果を出力)
        計算量: O(DNS解決 + ネットワークレイテンシ)
        メモリ: O(応答サイズ)
        """
        print(f"Fetching (safe): {url}")
    
        parsed_url = urlparse(url)
    
        # 1. スキームのホワイトリスト化: http/httpsのみ許可 [5]
    
        if parsed_url.scheme not in ['http', 'https']:
            print(f"Error: 不許可なスキーム '{parsed_url.scheme}' です。")
            return
    
        host = parsed_url.hostname
        if not host:
            print("Error: ホスト名が指定されていません。")
            return
    
        # 2. ホストの厳格な検証 (IPアドレス、予約済みIP、内部ホスト名の禁止) [5]
    
    
        # 注意: このクライアントサイドでのIP検証は完璧ではありません。
    
    
        # DNSリバインディング攻撃や、異なるIP解決(IPv4/IPv6)に対応するためには、
    
    
        # ネットワーク層でのフィルタリングや、アプリケーションが実際に接続するIPアドレスを
    
    
        # 事前に確認する、より複雑なロジックが必要です。
    
        try:
    
            # ホスト名がIPアドレス形式の場合のチェック
    
            ip_obj = ipaddress.ip_address(host)
            if ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_private:
                print(f"Error: 予約済み/内部IPアドレス '{host}' へのアクセスは許可されていません。")
                return
            if str(ip_obj) == "169.254.169.254": # クラウドメタデータIP [3]
                print(f"Error: クラウドメタデータIP '{host}' へのアクセスは許可されていません。")
                return
        except ValueError: # ホスト名がIPアドレス形式ではない場合
    
            # ホスト名がDNS解決されるIPアドレスをチェック (DNSリバインディング対策の一部)
    
    
            # socket.gethostbyname() は単一のIPを返すため、より厳密には gethostbyname_ex を利用し、
    
    
            # 取得した全てのIPをチェックする必要があります。
    
            try:
                resolved_ip = socket.gethostbyname(host)
                ip_obj_resolved = ipaddress.ip_address(resolved_ip)
                if ip_obj_resolved.is_loopback or ip_obj_resolved.is_link_local or ip_obj_resolved.is_private:
                    print(f"Error: 予約済み/内部IPアドレス '{resolved_ip}' への解決は許可されていません。")
                    return
                if str(ip_obj_resolved) == "169.254.169.254":
                    print(f"Error: クラウドメタデータIP '{resolved_ip}' への解決は許可されていません。")
                    return
            except socket.gaierror:
                print(f"Error: ホスト名 '{host}' を解決できませんでした。")
                return
            except Exception as e:
                print(f"Error during DNS resolution for '{host}': {e}")
                return
    
            # 許可された外部ドメインリストとの照合 (オプション)
    
    
            # ALLOWED_DOMAINS = ["example.com", "api.trusted.com"]
    
    
            # if not any(host.endswith(domain) for domain in ALLOWED_DOMAINS):
    
    
            #     print(f"Error: 不許可なドメイン '{host}' へのアクセスは許可されていません。")
    
    
            #     return
    
        # 3. リダイレクトの追跡を無効化または制限 [5]
    
    
        # 攻撃者がリダイレクトを利用して内部IPに誘導するのを防ぐ
    
        try:
            response = requests.get(url, timeout=5, allow_redirects=False) # リダイレクトを追跡しない
    
            # オプション: リダイレクトヘッダーをチェックし、安全なURLのみ追跡を許可する
    
            if response.status_code in (301, 302, 303, 307, 308):
                redirect_url = response.headers.get('Location')
                if redirect_url:
                    print(f"Redirect detected to: {redirect_url}. Not following due to allow_redirects=False.")
    
                    # 必要に応じて、ここで redirect_url を再度 safe_fetch_url で検証し、
    
    
                    # 安全と判断された場合にのみ再帰的に追跡するロジックを実装することも可能。
    
                return 
    
            print(f"Status: {response.status_code}")
            print(f"Content (first 100 chars): {response.text[:100]}")
        except requests.exceptions.RequestException as e:
            print(f"Error: {e}")
    
    if __name__ == "__main__":
    
        # 実行例: python safe.py https://example.com
    
    
        # 実行例 (ブロックされる): python safe.py http://169.254.169.254/latest/meta-data/
    
    
        # 実行例 (ブロックされる): python safe.py file:///etc/passwd
    
    
        # 実行例 (ブロックされる): python safe.py http://127.0.0.1
    
        if len(sys.argv) > 1:
            safe_fetch_url(sys.argv[1])
        else:
            print("Usage: python safe.py <url>")
            print("Example: python safe.py https://www.google.com")
            print("Example (blocked): python safe.py http://169.254.169.254/latest/meta-data/")
            print("Example (blocked): python safe.py file:///etc/passwd")
            print("Example (blocked): python safe.py http://127.0.0.1")
    
  2. ネットワーク分離と最小権限:

    • ファイアウォール/セキュリティグループ: アプリケーションサーバーからのアウトバウンドリクエストを厳しく制限し、必要な外部リソース(許可されたAPIエンドポイントなど)以外の内部ネットワークやインターネットへのアクセスをブロックします [3, 4]。

    • 最小権限の原則: サーバーやサービスアカウントには、その機能に必要な最小限の権限のみを付与します。例えば、外部リソースをフェッチするサービスに、クラウドメタデータサービスへのアクセス権限は与えないようにします [3, 4]。

    • IMDSv2の強制: AWSを利用している場合、Instance Metadata Service Version 2 (IMDSv2) の利用を強制します。IMDSv2はセッショントークンを要求するため、単純なSSRF攻撃では認証情報を直接窃取することが困難になります [3]。

  3. 応答内容の検証: サーバーからの応答が内部エラーメッセージや機密情報を含んでいないかを検証し、予期しない情報開示を防ぎます [1]。

運用対策

SSRFに対する運用対策は、継続的なセキュリティ強化のために不可欠です。

  1. 鍵/秘匿情報の取り扱い:

    • 専用のシークレット管理サービスを利用: APIキー、データベース認証情報、その他の機密情報は、AWS Secrets Manager、GCP Secret Manager、Azure Key Vaultなどの専用サービスで管理し、環境変数や設定ファイルに直接記述しないようにします [3, 4]。

    • 認証情報のローテーション: サービスアカウントやアプリケーションが使用する認証情報は、定期的にローテーションするポリシーを確立します。

    • 最小権限の原則: シークレットへのアクセス権限は、必要なサービスとユーザーにのみ付与し、最小限に制限します。

    • 監査ログ: シークレットへのアクセスやローテーション履歴を詳細に記録し、異常なアクティビティを監視します。

  2. 継続的な監査と監視:

    • アプリケーションが行う外部リクエスト、特にSSRFの対象となりうる機能からのリクエストは詳細なログとして記録し、監視システムで異常なパターン(例: 内部IPへの繰り返しアクセス、異常な量のデータ取得)を検知できるようにします [1, 6]。

    • 定期的なセキュリティ診断(ペネトレーションテスト、脆弱性スキャン)を実施し、新たなSSRF脆弱性やバイパス技術が適用されていないかを確認します。

  3. 開発者教育:

    • 開発チーム全体でSSRFの脅威と安全なコーディングプラクティスについて理解を深めるための教育プログラムを実施します。特に、ユーザー入力の取り扱い、URLパーシングの安全性、ネットワークアクセスに関するベストプラクティスを強調します。
  4. セキュリティパッチとアップデート:

    • 使用しているOS、Webサーバー、アプリケーションフレームワーク、ライブラリは常に最新のセキュリティパッチが適用されていることを確認します。古いバージョンのライブラリには、SSRFに関連する既知の脆弱性が存在する可能性があります [6]。

現場の落とし穴

  • 誤検知と検出遅延: ホワイトリスト/ブラックリストのルールが過度に厳格であると、正当なビジネスロジックをブロックし、誤検知を発生させる可能性があります。逆に、ルールの不備や新しいバイパス技術(例: IPアドレスのエンコード、DNSリバインディング、URLスキームの曖昧な解釈)に対して検出が遅れることがあります [2, 5]。

  • 可用性とのトレードオフ: 厳しすぎるネットワーク制限やWAFルールは、システム全体の可用性を損なう可能性があります。セキュリティとビジネス要件のバランスを考慮した設計が必要です [3]。

  • リダイレクトの悪用: 信頼されたドメインへのアクセスが許可されていても、そのドメインが内部IPアドレスへのリダイレクトを返すことで、SSRFが成立する場合があります。リダイレクトを無効化するか、リダイレクト先のURLも厳密に検証する必要があります [5]。

  • IPv4/IPv6の混同: IPv4アドレスだけでなく、IPv6アドレスやその変換形式(例: ::ffff:127.0.0.1)もSSRFのターゲットとなり得るため、両方に対応した検証が必要です [2]。

まとめ

SSRFは、内部システムへの侵入や機密情報漏洩の足がかりとなる重大な脆弱性です。この脅威に対抗するためには、単一の対策に依存するのではなく、入力検証の徹底、ネットワーク分離、最小権限の適用、クラウドメタデータサービス保護、そして継続的な監査と開発者教育を組み合わせた多層的なセキュリティ戦略が不可欠です。安全なコーディングプラクティスと堅牢なシステム設計により、SSRFのリスクを最小限に抑え、Webアプリケーションのセキュリティを向上させることができます。


参考文献:

  • [1] OWASP Foundation, “A10:2021-Server-Side Request Forgery (SSRF)”, 2021年11月1日. URL: https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/

  • [2] PortSwigger, “Server-side request forgery (SSRF)”, 2024年5月1日時点の内容に基づく. URL: https://portswigger.net/web-security/ssrf

  • [3] Amazon Web Services (AWS), “Preventing Server-Side Request Forgery (SSRF) in the Cloud”, 2022年10月31日. URL: https://aws.amazon.com/blogs/security/preventing-server-side-request-forgery-ssrf-in-the-cloud/

  • [4] Google Cloud, “Server-side request forgery (SSRF) and how to mitigate it”, 2022年11月18日. URL: https://cloud.google.com/blog/products/identity-security/server-side-request-forgery-ssrf-and-how-to-mitigate-it

  • [5] GitHub Security Lab, “Preventing SSRF”, 2023年4月11日. URL: https://securitylab.github.com/research/preventing-ssrf/

  • [6] MITRE, “CWE-918: Server-Side Request Forgery (SSRF)”, 2023年10月24日. URL: https://cwe.mitre.org/data/definitions/918.html

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました