説明
サーバーサイドリクエストフォージェリ(SSRF)は、攻撃者がユーザー入力を悪用し、サーバーに本来の用途にない宛先への要求を送らせる問題です。攻撃者が直接接続できない内部ネットワークや機密データへアクセスする経路になる可能性があります。
想定される影響
- 内部ネットワークへのアクセス: サーバーを経由して内部サービスに到達されるおそれがあります。
- 情報漏えい: 内部システムが返したデータを取得される可能性があります。
- サービス停止: 悪意のある要求によってサービスが停止するおそれがあります。
対処方法
- 外部へ要求を送る前に入力を検証してください。
- ユーザーからURLを受け取るより、識別子から固定URLを選ぶサーバー管理の対応表を優先してください。
- URLの入力が必要な場合は、標準のパーサーでスキームとホスト名を分離し、サーバーが管理する許可リストと正確に比較してください。
- 自動リダイレクトを無効にするか、毎回宛先を再検証してください。想定外のプライベートIP・リンクローカルアドレスへの名前解決や、意図しないプロキシの使用を防いでください。
例
変更前
python
# ユーザーが指定する送信先
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/fetch')
def fetch():
url = request.args.get('url')
response = requests.get(url)
return response.content
変更後
python
# 固定の宛先と応答制限
from flask import Flask, request, abort, Response
import requests
app = Flask(__name__)
@app.route('/fetch')
def fetch():
target_id = request.args.get('target')
# ユーザーは識別子だけを選び、実際のURLは固定の分岐で設定する
if target_id == "status":
url = "https://status.example.com/health"
elif target_id == "catalog":
url = "https://api.example.com/v1/catalog"
else:
abort(400)
max_response_bytes = 1024 * 1024
chunks = []
total = 0
with requests.get(
url, timeout=3, allow_redirects=False, stream=True
) as response:
for chunk in response.iter_content(chunk_size=64 * 1024):
total += len(chunk)
if total > max_response_bytes:
abort(502)
chunks.append(chunk)
status_code = response.status_code
proxied = Response(
b''.join(chunks),
status=status_code,
content_type="application/octet-stream",
)
proxied.headers["X-Content-Type-Options"] = "nosniff"
return proxied
説明:
- 変更前: 未検証の入力が送信先を決めるため、SSRFに悪用されるおそれがあります。
- 変更後: 許可した識別子からサーバー管理の固定URLを選び、それ以外を拒否します。リダイレクトを無効にし、接続・読み取りの待機時間と1 MiBの応答サイズ上限を設定し、コンテンツタイプと
nosniffヘッダーを固定します。timeout=3は処理全体の3秒制限ではないため、必要な全体の期限やネットワーク送信ポリシーは別途構成してください。