WebアプリケーションのSSRF攻撃対策

Tech

WebアプリケーションにおけるSSRF攻撃対策の実践

WebアプリケーションのSSRF(Server-Side Request Forgery)攻撃は、外部からの入力値を利用して、アプリケーションが本来アクセスすべきではない内部リソースや外部サービスへリクエストを強制させる脅威である。これにより、機密情報の窃取、内部システムのスキャン、ポートスキャン、あるいは他の内部サービスへの足がかりとして悪用される。

脅威モデル

攻撃者は、公開されたWebアプリケーションが提供する、外部URLを処理する機能(例: サムネイル生成、PDF変換、Webhooks、画像プロキシ)を悪用する。この機能を介して、Webアプリケーションが稼働するサーバーの内部ネットワークにある秘匿情報(クラウドプロバイダのメタデータサービス、内部API、データベース、イントラネットサービスなど)へアクセスを試みる。攻撃の目的は、これらの情報を窃取するか、あるいは内部ネットワークへの偵察、さらには他のシステムへの二次攻撃の起点とすることである。

攻撃シナリオ

攻撃者は、例えば次のような悪意あるURLをWebアプリケーションに送信する。

  1. クラウドメタデータサービスへのアクセス: http://169.254.169.254/latest/meta-data/iam/security-credentials/(AWSの場合)
  2. ローカルファイル読み取り: file:///etc/passwd
  3. 内部APIへの不正アクセス: http://localhost/admin/users?action=delete&id=1
  4. 内部ポートスキャン: http://192.168.1.1:8080/
  5. Gopherプロトコルによる内部サービス連携: gopher://localhost:6379/_*1%0d%0a$4%0d%0aINFO%0d%0a(Redisコマンド実行)

WebアプリケーションはこれらのURLを検証せず、指定されたリソースへのリクエストを実行し、その結果(エラーメッセージやレスポンスボディ)を攻撃者に返すことで、情報漏洩や不正操作が発生する。

graph TD
    A["攻撃者"] --> B["悪意あるURLを含むリクエスト"];
    B --> C{"Webアプリケーション (脆弱な機能)"};
    C -- URL検証なし --> D{"Webアプリケーション (サーバーサイド)"};
    D -- 内部リソースへの不正リクエスト --> E["内部ネットワーク/システム (クラウドメタデータ、DB、APIなど)"];
    E -- 秘匿情報/レスポンス --> D;
    D -- 攻撃結果の返却 --> A;

    subgraph 検出/緩和フェーズ
        F{"Webアプリケーション (安全な実装)"};
        C -- URL検証実施 --> F;
        F -- 許可された外部リソースへのリクエスト --> G["外部システム"];
    end

    style C fill:#FFDDDD,stroke:#FF0000,stroke-width:2px;
    style D fill:#FFDDDD,stroke:#FF0000,stroke-width:2px;
    style F fill:#DDFFDD,stroke:#00AA00,stroke-width:2px;

検出/緩和

SSRF対策は多層的なアプローチが必要である。

1. URLの厳格な検証(ホワイトリスト方式)

入力されたURLは、スキーム、ホスト名、IPアドレス、ポートをすべて検証する。

  • スキームの制限: httpまたはhttpsのみを許可し、file://gopher://ftp://data://などを拒否する。
  • ホスト名の検証: 許可されたドメイン(ホワイトリスト)のみを通過させる。
  • IPアドレスの検証: DNS解決後のIPアドレスが、ループバックアドレス(127.0.0.1::1)、プライベートIPアドレス(RFC1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)、リンクローカルアドレス(169.254.0.0/16)に該当しないことを確認する。DNSリバインディング攻撃を考慮し、名前解決後のIPアドレスを検証することが必須。
  • ポートの制限: 通常のWeb通信に必要なポート(例: 80, 443)のみを許可する。

誤用例 (Python):

import requests
user_supplied_url = "http://169.254.169.254/latest/meta-data/"
response = requests.get(user_supplied_url) # 検証なしにリクエスト
print(response.text) # 機密情報が漏洩する可能性

安全な代替 (Python):

import requests
from urllib.parse import urlparse
import ipaddress
import socket

def is_safe_url(url):
    try:
        parsed_url = urlparse(url)
        if parsed_url.scheme not in ["http", "https"]:
            print(f"Danger: Invalid scheme '{parsed_url.scheme}'")
            return False

        if not parsed_url.hostname:
            print("Danger: No hostname provided")
            return False

        # ホワイトリストによるドメイン検証 (例)
        allowed_domains = ["example.com", "api.partner.com"]
        if parsed_url.hostname not in allowed_domains:
            print(f"Danger: Hostname '{parsed_url.hostname}' not in whitelist")
            # さらに、DNS解決してIPアドレス検証
            try:
                ip_addresses = socket.gethostbyname_ex(parsed_url.hostname)[2]
                for ip_addr_str in ip_addresses:
                    ip_addr = ipaddress.ip_address(ip_addr_str)
                    if ip_addr.is_loopback or ip_addr.is_private or \
                       (ip_addr.version == 4 and ip_addr_str.startswith('169.254.')): # Link-local
                        print(f"Danger: Resolved IP '{ip_addr_str}' is internal/loopback")
                        return False
            except socket.gaierror:
                print(f"Danger: Could not resolve hostname '{parsed_url.hostname}'")
                return False

        # 特定のポートのみ許可 (例)
        if parsed_url.port and parsed_url.port not in [80, 443]:
            print(f"Danger: Invalid port '{parsed_url.port}'")
            return False

        return True
    except Exception as e:
        print(f"Error during URL parsing: {e}")
        return False

user_supplied_url = "http://169.254.169.254/latest/meta-data/"
# user_supplied_url = "https://example.com/data" # 安全な例

if is_safe_url(user_supplied_url):
    print("URL is safe. Making request...")
    response = requests.get(user_supplied_url, timeout=5)
    print(response.text)
else:
    print("Unsafe URL. Request blocked.")

2. HTTPリダイレクトの追跡停止/制限

一部のHTTPクライアントライブラリは、リダイレクトを自動的に追跡する。悪意のあるURLが外部ドメインを指し、その後に内部IPアドレスへリダイレクトされることでSSRF対策を迂回される可能性がある。リダイレクトの追跡を無効にするか、リダイレクト後のURLも同様にis_safe_urlで検証する。

# requestsライブラリの場合、allow_redirects=False を指定
if is_safe_url(user_supplied_url):
    response = requests.get(user_supplied_url, allow_redirects=False, timeout=5)
    # 必要に応じて、レスポンスヘッダのLocationを検証し、再度リクエストを行うロジックを追加

3. ネットワークACL / ファイアウォール

アプリケーションサーバーからのアウトバウンドトラフィックを制限する。内部ネットワークへのアクセスは必要最小限に絞り込み、特にクラウドプロバイダのメタデータサービスやデータベースなどの機密性の高いリソースへのアクセスをネットワークレベルでブロックする。

# 例: Linuxのiptablesで特定のプライベートIP範囲へのHTTP/HTTPSアウトバウンドをブロック
sudo iptables -A OUTPUT -d 10.0.0.0/8 -p tcp -m multiport --dports 80,443 -j DROP
sudo iptables -A OUTPUT -d 172.16.0.0/12 -p tcp -m multiport --dports 80,443 -j DROP
sudo iptables -A OUTPUT -d 192.168.0.0/16 -p tcp -m multiport --dports 80,443 -j DROP
sudo iptables -A OUTPUT -d 169.254.0.0/16 -p tcp -m multiport --dports 80,443 -j DROP

クラウド環境では、セキュリティグループやNACLを活用する。

4. WAF (Web Application Firewall)

不審なURLパターン(例: 127.0.0.1169.254.169.254file:///などの文字列を含むリクエスト)をブロックするようにWAFを設定する。ただし、WAFはあくまで補助的な対策であり、アプリケーションレベルでの検証が最も重要である。

運用対策

鍵/秘匿情報の取り扱い

  • 集中管理: データベース認証情報、APIキー、クラウド認証情報などは、環境変数、シークレットマネージャー(AWS Secrets Manager, HashiCorp Vaultなど)、またはOSのセキュアなストレージに保存し、コードにハードコードしない。
  • SSRFがアクセス可能なサービス: クラウドのIAMロールやインスタンスプロファイルを使用し、アプリケーションに必要最小限の権限のみを付与する。特にインスタンスメタデータサービスへのアクセスは、原則として読み取り専用権限のみとし、特権操作は許可しない。AWS IMDSv2のようにセッションベースの認証を強制するメカニズムを利用する。

ローテーション

アプリケーションが使用するAPIキーやデータベースパスワードなどの認証情報を定期的にローテーションする。これにより、万一SSRFによって認証情報が漏洩した場合でも、その有効期間を短縮し、被害を軽減する。

最小権限

Webアプリケーションが内部リソースにアクセスする際の実行ユーザー、サービスアカウント、IAMロールには、必要最小限の権限のみを付与する。たとえば、画像のダウンロード機能であれば、オブジェクトストレージへの読み取り権限のみを与える。内部APIへのアクセスも、関連する特定のエンドポイントと操作のみを許可する。

監査

SSRF攻撃の試行や成功を検出するためには、詳細なログ記録と監視が不可欠である。

  • アクセスログ: Webアプリケーションが外部リソースへリクエストを送信する際のURL、送信元IPアドレス、時刻などをログに記録する。
  • セキュリティログ: URL検証に失敗したリクエストや、不審なパターンを含むリクエストをセキュリティログとして記録する。
  • SIEM連携: これらのログをSIEM(Security Information and Event Management)システムに集約し、異常なアクセスパターン、内部IPアドレスへの繰り返しアクセス試行、非標準ポートへの接続などを検知してアラートを発報する。

現場の落とし穴

  • DNSリバインディング: 攻撃者が短時間でDNSレコードを外部IPから内部IPに切り替えることで、URL検証を迂回する手法。DNSキャッシュのTTLを低く設定したり、名前解決後のIPアドレスを常に検証するロジックを実装することで対策する。
  • 可用性とのトレードオフ: 厳格すぎるURL検証ルールやWAF設定は、正当な外部連携サービスへのアクセスを誤ってブロックし、サービス障害を引き起こす可能性がある。本番環境への適用前に十分なテストと、監視体制の確立が重要である。
  • 検出遅延/誤検知: 大量のログからSSRF攻撃の試行を正確に特定することは困難な場合がある。過剰なアラートはノイズとなり、重要な脅威を見逃す原因となるため、アラートルールの継続的なチューニングが必要。
  • 複雑なURLパース: URLエンコード、ダブルエンコード、@を含むユーザー情報など、URLのさまざまな表現を正しく解釈し、検証ロジックをすり抜けさせないように注意が必要である。

まとめ

SSRF攻撃はWebアプリケーションの内部環境に深く侵入しうる深刻な脅威である。これを防ぐためには、コードレベルでの厳格なURL検証(ホワイトリスト方式)、ネットワークレベルでのアウトバウンド制限、最小権限の原則に基づく認証情報管理、そして継続的な監視とログ分析といった多層的な防御戦略が不可欠である。これらの対策を組み合わせることで、SSRF攻撃のリスクを最小化し、アプリケーションと内部システムのセキュリティを向上させる。

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

コメント

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