サーバーサイドリクエストフォージェリの防止

Server-Side Request Forgery Prevention Cheat Sheet

原典
Server-Side Request Forgery Prevention Cheat Sheet(OWASP Cheat Sheet Series)
原文
GitHub 上の Markdown
底本
bac04fb5(2026-07-29 時点)
ライセンス
CC BY-SA 4.0(原典と同一。訳文も同ライセンスで再配布できます)

はじめに

このチートシートの目的は、サーバーサイドリクエストフォージェリ(SSRF)攻撃に対する保護について助言することである。

このチートシートは防御の観点に焦点を置き、この攻撃をどう実行するのかは説明しない。 セキュリティ研究者 Orange Tsai講演と、この文書が、この種の攻撃を行う手法を扱っている。

背景

SSRF は、アプリケーションを濫用して内部や外部のネットワーク、あるいはその機器自体と対話させる攻撃経路である。 この経路を可能にする要因の一つは URL の扱いの誤りであり、次の例に現れる。

  • 外部サーバー上の画像(アプリケーションが取得して使うアバターの画像 URL を利用者が入力する場合など)
  • 独自の WebHook(利用者が Webhook のハンドラやコールバック URL を指定する場合)
  • 特定の機能を提供するために別のサービスと対話する内部のリクエスト。多くの場合、処理のために利用者のデータが一緒に送られ、扱いが不適切だと特定のインジェクション攻撃を成立させうる。

SSRF のよくある流れの概観

SSRF のよくある流れ

注記

  • SSRF は HTTP プロトコルに限られない。一般に最初のリクエストは HTTP であるが、アプリケーション自体が二番目のリクエストを行う場合、異なるプロトコル(FTP、SMB、SMTP など)とスキーム(file://phar://gopher://data://dict:// など)を使いうる。
  • アプリケーションが XML 外部実体(XXE)インジェクションに対して脆弱であれば、それを悪用して SSRF 攻撃を行える。XXE への露出を防ぐ方法は XML 外部実体参照の防止を参照する。

場合分け

アプリケーションの機能と要件に応じて、SSRF が起こりうる基本的な場合は二つある。

  • アプリケーションが識別済みで信頼できるアプリケーションにのみリクエストを送れる場合。許可リストの方法が使える場合である。
  • アプリケーションが任意の外部の IP アドレスやドメイン名へリクエストを送れる場合。許可リストの方法が使えない場合である。

この二つは大きく異なるので、このチートシートではそれぞれに対する防御を分けて述べる。

場合 1。アプリケーションが識別済みで信頼できるアプリケーションにのみリクエストを送れる

アプリケーションが特定の作業を行うために、しばしば別のネットワークにある他のアプリケーションへリクエストを行う必要があることがある。 業務上の要件に応じて、その機能が働くには利用者の入力が必要になる。

利用者から氏名や生年月日などの個人情報を受け取り、それを使って内部の人事システムにプロフィールを作る Web アプリケーションを例にとる。 設計上、その Web アプリケーションはそのデータを処理するために、人事システムが理解するプロトコルで通信しなければならない。 利用者は人事システムに直接到達できないが、利用者の情報を受け取る役割の Web アプリケーションが SSRF に対して脆弱であれば、利用者はそれを足がかりに人事システムへアクセスできる。 利用者は Web アプリケーションを人事システムへのプロキシとして利用する。

脆弱なアプリケーションが呼び出す内部のアプリケーションは技術的および業務的な流れの中で明確に識別されているので、許可リストの方法は実行可能な選択である。 必要な呼び出しは、識別済みで信頼できるアプリケーションのあいだにのみ向けられると言える。

利用できる保護

アプリケーションの層とネットワークの層で、いくつもの保護策が可能である。 多層防御の原則を適用するため、両方の層をこの種の攻撃に対して堅牢にする。

アプリケーションの層

最初に思い浮かぶ保護の水準は入力値の検証である。

そこで次の疑問が浮かぶ。 この入力検証をどう行うのか。

Orange Tsai講演で示すように、使用するプログラミング言語によってはパーサが濫用されうる。 一つの対策は、入力検証を行うときに許可リストの方法を適用することである。 多くの場合、利用者から期待される情報の形式は全体としてわかっているからである。

内部のアプリケーションへ送るリクエストは、次の情報に基づく。

  • 業務データを含む文字列
  • IP アドレス(V4 または V6)
  • ドメイン名
  • URL

注記:Web クライアントにおけるリダイレクトの追従を無効にして、この文書Exploitation tricks > Bypassing restrictions > Input validation > Unsafe redirect の節で述べられている入力検証の回避を防ぐ。

文字列

SSRF の文脈では、入力文字列が期待される業務上および技術上の形式に従っていることを確かめる検証を加えられる。

入力データの形式が単純であれば(トークン、郵便番号など)、正規表現を使って、受け取ったデータが安全性の観点から正当であることを確かめられる。 そうでない場合は、string オブジェクトから使えるライブラリで検証を行うべきである。 複雑な形式に対する正規表現は保守が難しく、誤りが生じやすいからである。

利用者の入力はネットワークに関わらないもので、利用者の個人情報からなると想定する。

例を示す。

//Regex validation for a data having a simple format
if(Pattern.matches("[a-zA-Z0-9\\s\\-]{1,50}", userInput)){
    //Continue the processing because the input data is valid
}else{
    //Stop the processing and reject the request
}
IP アドレス

SSRF の文脈では、行うべき検証が二つある。

  1. 与えられたデータが正当な IP v4 または v6 のアドレスであることを確かめる。
  2. 与えられた IP アドレスが、識別済みで信頼できるアプリケーションの IP アドレスのいずれかであることを確かめる。

第一の検証の層は、使用する技術に応じて、IP アドレスの形式の安全性を確かめるライブラリで適用できる(IP アドレスの形式の扱いを委譲し、十分に試された検証機能を活用するため、ここではライブラリを使う方法を提案する)。

提案するライブラリについては、この記事で述べられている回避(16 進、8 進、Dword、URL、混合のエンコーディング)への露出を検証済みである。

  • JavaApache Commons Validator ライブラリの InetAddressValidator.isValid メソッド。
    • 16 進、8 進、Dword、URL、混合のエンコーディングによる回避に露出していない
  • .NET:SDK の IPAddress.TryParse メソッド。
    • 16 進、8 進、Dword、混合のエンコーディングによる回避に露出しているが、URL エンコーディングには露出していない
    • ここでは許可リストを用いるので、回避の試みは許可された IP アドレスの一覧との比較の際に遮断される。
  • JavaScriptip-address ライブラリ。
    • 16 進、8 進、Dword、URL、混合のエンコーディングによる回避に露出していない
  • Ruby:SDK の IPAddr クラス。
    • 16 進、8 進、Dword、URL、混合のエンコーディングによる回避に露出していない

許可リストとの比較には、メソッドやライブラリの出力値を IP アドレスとして使う。

受け取った IP アドレスの正当性を確かめたあと、第二の検証の層を適用する。 識別済みで信頼できるアプリケーションのすべての IP アドレス(回避を避けるため v4 と v6 の両方)を決めたうえで、許可リストを作る。 正当な IP をその一覧と照合し、内部のアプリケーションとの通信を確かめる(大文字小文字を区別する厳密な文字列比較で行う)。

ドメイン名

ドメイン名を検証しようとするとき、そのドメインの存在を確かめるために DNS の解決を行うことが思い浮かぶ。 一般にそれは悪い考えではないが、ドメイン名の解決に使う DNS サーバーの設定に応じて、アプリケーションを攻撃にさらすことになる。

  • 外部の DNS リゾルバに情報を露出しうる。
  • 攻撃者が正当なドメイン名を内部の IP アドレスに結びつけるのに使いうる。この文書Exploitation tricks > Bypassing restrictions > Input validation > DNS pinning の節を参照する。
  • 攻撃者が悪意あるペイロードを内部の DNS リゾルバや、アプリケーションが DNS の通信を扱うために使う API(SDK やサードパーティ)へ届け、それらの構成要素の脆弱性を突く可能性がある。

SSRF の文脈では、行うべき検証が二つある。

  1. 与えられたデータが正当なドメイン名であることを確かめる。
  2. 与えられたドメイン名が、識別済みで信頼できるアプリケーションのドメイン名のいずれかであることを確かめる(ここで許可リストが働く)。

IP アドレスの検証と同様に、第一の検証の層は、使用する技術に応じてドメイン名の形式の安全性を確かめるライブラリで適用できる(ドメイン名の形式の扱いを委譲し、十分に試された検証機能を活用するため、ここではライブラリを使う方法を提案する)。

提案する関数が DNS の解決の問い合わせを行わないことを検証済みである。

Ruby で提案した正規表現を実行する例を示す。

domain_names = ["owasp.org","owasp-test.org","doc-test.owasp.org","doc.owasp.org",
                "<script>alert(1)</script>","<script>alert(1)</script>.owasp.org"]
domain_names.each { |domain_name|
    if ( domain_name =~ /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/ )
        puts "[i] #{domain_name} is VALID"
    else
        puts "[!] #{domain_name} is INVALID"
    end
}
$ ruby test.rb
[i] owasp.org is VALID
[i] owasp-test.org is VALID
[i] doc-test.owasp.org is VALID
[i] doc.owasp.org is VALID
[!] <script>alert(1)</script> is INVALID
[!] <script>alert(1)</script>.owasp.org is INVALID

受け取ったドメイン名の正当性を確かめたあと、第二の検証の層を適用する。

  1. 識別済みで信頼できるすべてのアプリケーションのドメイン名で許可リストを作る。
  2. 受け取ったドメイン名がその許可リストに含まれることを確かめる(大文字小文字を区別する厳密な文字列比較で行う)。

しかし残念ながら、この時点でもアプリケーションは前述の文書で言及されている DNS pinning による回避に対して脆弱である。 業務のコードが実行されるときに DNS の解決が行われるからである。 この問題に対処するには、ドメイン名の検証に加えて次の措置を採らなければならない。

  1. 自組織に属するドメインが、DNS リゾルバの連鎖の中で最初に自組織の内部 DNS サーバーによって解決される状態にする。
  2. 許可リストのドメインを監視し、いずれかが次のものに解決されたときに検出できるようにする。
    • ローカルの IP アドレス(v4 と v6)
    • 自組織に属さないドメインについて、自組織の内部 IP(私用 IP の範囲にあると想定される)

上記の監視の出発点として、次の Python3 のスクリプトが使える。

# Dependencies: pip install ipaddress dnspython
import ipaddress
import dns.resolver

# Configure the allowlist to check
DOMAINS_ALLOWLIST = ["owasp.org", "labslinux"]

# Configure the DNS resolver to use for all DNS queries
DNS_RESOLVER = dns.resolver.Resolver()
DNS_RESOLVER.nameservers = ["1.1.1.1"]

def verify_dns_records(domain, records, type):
    """
    Verify if one of the DNS records resolve to a non public IP address.
    Return a boolean indicating if any error has been detected.
    """
    error_detected = False
    if records is not None:
        for record in records:
            value = record.to_text().strip()
            try:
                ip = ipaddress.ip_address(value)
                # See https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Address.is_global
                if not ip.is_global:
                    print("[!] DNS record type '%s' for domain name '%s' resolve to
                    a non public IP address '%s'!" % (type, domain, value))
                    error_detected = True
            except ValueError:
                error_detected = True
                print("[!] '%s' is not valid IP address!" % value)
    return error_detected

def check():
    """
    Perform the check of the allowlist of domains.
    Return a boolean indicating if any error has been detected.
    """
    error_detected = False
    for domain in DOMAINS_ALLOWLIST:
        # Get the IPs of the current domain
        # See https://en.wikipedia.org/wiki/List_of_DNS_record_types
        try:
            # A = IPv4 address record
            ip_v4_records = DNS_RESOLVER.query(domain, "A")
        except Exception as e:
            ip_v4_records = None
            print("[i] Cannot get A record for domain '%s': %s\n" % (domain,e))
        try:
            # AAAA = IPv6 address record
            ip_v6_records = DNS_RESOLVER.query(domain, "AAAA")
        except Exception as e:
            ip_v6_records = None
            print("[i] Cannot get AAAA record for domain '%s': %s\n" % (domain,e))
        # Verify the IPs obtained
        if verify_dns_records(domain, ip_v4_records, "A")
        or verify_dns_records(domain, ip_v6_records, "AAAA"):
            error_detected = True
    return error_detected

if __name__== "__main__":
    if check():
        exit(1)
    else:
        exit(0)
URL

利用者から完全な URL を受け取らない。 URL は検証が難しく、Orange Tsai講演が示すように、使用する技術によってはパーサが濫用されうる。

ネットワークに関する情報がどうしても必要なら、正当な IP アドレスまたはドメイン名のみを受け取る。

ネットワークの層

ネットワークの層の安全対策の目的は、脆弱なアプリケーションが任意のアプリケーションへ呼び出しを行うことを防ぐことである。 そのアプリケーションのネットワークアクセスを、通信すべき相手のみに限定するため、許可された経路のみを使えるようにする。

正当な流れを定義するために、専用の機器としての、あるいは OS が提供するファイアウォールの構成要素をここで使う。

以下の図では、ファイアウォールの構成要素を活用してアプリケーションのアクセスを限定し、ひいては SSRF に脆弱なアプリケーションの影響を限定している。

防ぎたい流れについてのネットワーク層の保護。場合 1

ネットワークの分離実装上の助言も参照)も活用できる。 正当でない呼び出しをネットワークの水準そのもので直接遮断するため、これは強く推奨される。

場合 2。アプリケーションが任意の外部の IP アドレスやドメイン名へリクエストを送れる

これは利用者が外部のリソースへの URL を制御でき、アプリケーションがその URL へリクエストを行う場合に生じる(WebHook など)。 ここでは許可リストを使えない。 IP やドメインの一覧が事前にわからないことが多く、動的に変わるからである。

この場面での外部は、内部のネットワークに属さず、公衆のインターネットを経由して到達するべき IP を指す。

したがって脆弱なアプリケーションからの呼び出しは次のようになる。

  • 企業の全体のネットワークの内部にある IP やドメインを標的としていない
  • その呼び出しが正当に開始されたことを証明するため、脆弱なアプリケーションと想定される IP やドメインのあいだで定めた取り決めを使う。

アプリケーションの層で URL を遮断することの難しさ

上述のアプリケーションの業務要件に基づけば、許可リストの方法は妥当な解決策にならない。 拒否リストの方法が突破できない壁ではないと知っていても、この場面ではそれが最良の解決策である。 それはアプリケーションに、何をすべきでないのかを伝えるものである。

アプリケーションの層で URL を絞り込むことが難しい理由を挙げる。

  • アプリケーションがコードの水準で、与えられた IP(v4 と v6)が公式の私用ネットワークの範囲localhostIPv4/v6 のリンクローカルアドレスも含む)に属さないことを検出できなければならない。この種の検証の機能を組み込みで提供する SDK は多くなく、その落とし穴とありうる値のすべてを理解する作業は開発者に委ねられるので、負担の大きい仕事になる。
  • ドメイン名についても同様である。企業はすべての内部のドメイン名の一覧を維持し、与えられたドメイン名が内部のものかをアプリケーションが確かめられる中央のサービスを提供しなければならない。この検証のためにアプリケーションが内部の DNS リゾルバへ問い合わせることはできるが、その内部の DNS リゾルバは外部のドメイン名を解決してはならない。

利用できる保護

以下の節では、前述の例と同じ想定を置く。

アプリケーションの層

場合 1 と同じく、標的のアプリケーションへ送るリクエストを作るために IP アドレス または ドメイン名 が必要であると想定する。

三種類のデータに対して場合 1 で示した入力データの第一の検証はこの場合も同じであるが、第二の検証は異なる。 ここでは拒否リストの方法を使わなければならない。

リクエストの正当性の証明について:リクエストを受け取る標的のアプリケーションは、ランダムなトークン(英数字 20 文字など)を生成しなければならない。正当なリクエストを行うには、呼び出し側がそれを渡す必要がある(本体のパラメータ経由で渡し、そのパラメータ名もアプリケーション自身が定め、文字集合 [a-z]{1,10} のみを許可する)。受け取り側のエンドポイントは HTTP POST のリクエストのみを受け付けなければならない。

検証の流れ(検証の段階のいずれかが失敗したらリクエストを拒否する)

  1. アプリケーションは標的のアプリケーションの IP アドレスまたはドメイン名を受け取り、前述の節で挙げたライブラリや正規表現を用いて入力データに第一の検証を適用する。
  2. 次の拒否リストの方法を用いて、標的のアプリケーションの IP アドレスまたはドメイン名に第二の検証を適用する。
    • IP アドレスの場合
      • それが公衆のものであることをアプリケーションが確かめる(次の段落の Python のコード例にある手がかりを参照)。
    • ドメイン名の場合
      1. 内部のドメイン名のみを解決する DNS リゾルバに対してそのドメイン名の解決を試み、それが公衆のものであることをアプリケーションが確かめる。ここでは、そのドメインを知らないという応答が返らなければならない。受け取るべき値は公衆のドメインだからである。
      2. 前述の文書で述べられている DNS pinning 攻撃を防ぐため、アプリケーションは与えられたドメイン名の背後にあるすべての IP アドレスを取得し(IPv4 と IPv6 のために AAAAA のレコードを取る)、IP アドレスについて前項で述べた検証と同じものを適用する。
  3. アプリケーションは、リクエストに使うプロトコルを専用の入力パラメータ経由で受け取り、許可されたプロトコルの一覧(HTTP または HTTPS)に対してその値を検証する。
  4. アプリケーションは、標的のアプリケーションへ渡すトークンのパラメータ名を専用の入力パラメータ経由で受け取り、文字集合 [a-z]{1,10} のみを許可する。
  5. アプリケーションは、トークンそのものを専用の入力パラメータ経由で受け取り、文字集合 [a-zA-Z0-9]{20} のみを許可する。
  6. アプリケーションは、正当な呼び出しに必要な業務データを受け取り、安全性の観点から検証する。
  7. アプリケーションは検証済みの情報のみを使って HTTP POST のリクエストを組み立て、送信する(使う Web クライアントでリダイレクトの追従を無効にすることを忘れない)。
ネットワークの層

前述のネットワークの層と同様である。

AWS における IMDSv2

クラウドの環境では、メタデータのサービス(AWS Instance Metadata Service、Azure Instance Metadata Service、GCP のメタデータサーバーなど)から資格情報とアクセストークンを取得して盗むために SSRF がしばしば使われる。

IMDSv2 は、AWS における追加の多層防御の仕組みであり、SSRF の一部の事例を緩和する。

この保護を活用するには IMDSv2 へ移行し、古い IMDSv1 を無効にする。 さらに詳しくは AWS の文書を参照する。

拒否リスト(最後の手段)

拒否リストは回避されやすい。許可リストを優先する。

避けられない場合は、少なくとも次の範囲を遮断する。

サービス遮断する IP やドメイン
AWS IMDS169.254.169.254metadata.amazonaws.com
GCP のメタデータmetadata.google.internal169.254.169.254
Azure IMDS169.254.169.254
localhost127.0.0.0/80.0.0.0/8::1/128
RFC1918 の私用アドレス10.0.0.0/8172.16.0.0/12192.168.0.0/16
マルチキャスト224.0.0.0/4ff00::/8

本番の完全な例ComputerCraft の SSRF 拒否リスト

出典

Semgrep の規則

Semgrep は、オフラインの静的解析のためのコマンドラインの道具である。 あらかじめ用意された規則や独自の規則を使って、コードと安全性の基準をコードベースに強制できる。 SSRF に関する Semgrep の規則を活用すれば、SSRF の脆弱性の可能性を効果的に特定し調査できる。

参考資料

図に使った道具とコード

SSRF のよくある流れの Mermaid のコード(このチートシートに挿入した PNG 画像は画面の取り込みで作られている)。

sequenceDiagram
    participant Attacker
    participant VulnerableApplication
    participant TargetedApplication
    Attacker->>VulnerableApplication: Crafted HTTP request
    VulnerableApplication->>TargetedApplication: Request (HTTP, FTP...)
    Note left of TargetedApplication: Use payload included<br>into the request to<br>VulnerableApplication
    TargetedApplication->>VulnerableApplication: Response
    VulnerableApplication->>Attacker: Response
    Note left of VulnerableApplication: Include response<br>from the<br>TargetedApplication

防ぎたい流れについてのネットワーク層の保護。場合 1」の図の Draw.io の XML コード(このチートシートに挿入した PNG 画像は画面の取り込みで作られている)。