🔌
API セキュリティ(REST・GraphQL・OWASP API Top 10)
OWASP API Security Top 10(2023)
API は現代の Webアプリの中核だが、従来の OWASP Top 10 とは異なる脅威が存在する。
| # | カテゴリ | 概要 |
|---|---|---|
| API1 | Broken Object Level Authorization | 他ユーザーのオブジェクトを ID で直接取得できる(IDOR) |
| API2 | Broken Authentication | 弱い認証・トークン管理の欠陥 |
| API3 | Broken Object Property Level Authorization | 更新可能なフィールドを制限していない(Mass Assignment) |
| API4 | Unrestricted Resource Consumption | レートリミットなしで無制限にリソースを消費 |
| API5 | Broken Function Level Authorization | 管理者機能へのアクセス制御の欠陥 |
| API6 | Unrestricted Access to Sensitive Business Flows | ビジネスロジックの悪用(大量注文・OTP ブルートフォース) |
| API7 | Server Side Request Forgery(SSRF) | サーバーから内部リソースへの不正リクエスト |
| API8 | Security Misconfiguration | デフォルト設定・不要な機能の有効化 |
| API9 | Improper Inventory Management | 旧バージョン API・シャドウ API の放置 |
| API10 | Unsafe Consumption of APIs | 外部 API から受け取ったデータを無検証で使用 |
API1: Broken Object Level Authorization(IDOR)
# 危険: オブジェクト ID を検証なしで使う
@app.route('/api/v1/orders/<int:order_id>')
@jwt_required
def get_order(order_id):
order = Order.query.get(order_id)
return jsonify(order.to_dict())
# /api/v1/orders/1001 → 他人の注文が取れる
# 安全: ログインユーザーのリソースかを常に検証
@app.route('/api/v1/orders/<int:order_id>')
@jwt_required
def get_order(order_id):
order = Order.query.filter_by(
id=order_id,
user_id=get_jwt_identity() # ← JWT から取得したユーザー ID で絞り込む
).first_or_404()
return jsonify(order.to_dict())
設計の鉄則:
・すべてのオブジェクト取得・更新・削除でオーナーシップを検証する
・数値 ID の代わりに UUID(推測困難)を使う
・データ層(クエリ)でフィルタする(コントローラー層の if 文だけに頼らない)
API3: Mass Assignment(プロパティレベルの認可ミス)
ユーザーが送ってきた JSON をそのままモデルに渡すと、想定外のフィールドを書き換えられる。
# 危険: リクエストボディをそのままモデルに渡す
@app.route('/api/v1/users/me', methods=['PUT'])
@jwt_required
def update_profile():
user = User.query.get(get_jwt_identity())
user.update(**request.json) # ← {"role": "admin"} を送られると管理者に昇格できる
db.session.commit()
# 安全: 更新を許可するフィールドを明示的にホワイトリスト化
ALLOWED_FIELDS = {'display_name', 'bio', 'avatar_url'}
@app.route('/api/v1/users/me', methods=['PUT'])
@jwt_required
def update_profile():
user = User.query.get(get_jwt_identity())
data = {k: v for k, v in request.json.items() if k in ALLOWED_FIELDS}
user.update(**data)
db.session.commit()
# Pydantic でスキーマ定義(FastAPI の場合)
from pydantic import BaseModel
from typing import Optional
class UserUpdateRequest(BaseModel):
display_name: Optional[str] = None
bio: Optional[str] = None
avatar_url: Optional[str] = None
# role, is_admin などの機密フィールドは定義しない → 自動的に無視される
@app.put('/api/v1/users/me')
def update_profile(body: UserUpdateRequest, user=Depends(get_current_user)):
user.update(**body.model_dump(exclude_none=True))
API4: Unrestricted Resource Consumption(レートリミット)
攻撃パターン:
・OTP ブルートフォース: /api/auth/verify-otp を高速リクエスト
・クレデンシャルスタッフィング: /api/auth/login に大量のパスワードリスト
・DDoS: 計算コストの高い API(検索・PDF生成等)を大量呼び出し
・課金系 API の不正呼び出し: SMS 送信・メール送信 API を悪用
# Flask-Limiter を使ったレートリミット
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# ログインエンドポイント: 厳しく制限
@app.route('/api/auth/login', methods=['POST'])
@limiter.limit("5 per minute") # 1分あたり5回まで
def login():
...
# OTP 検証: さらに厳しく(ブルートフォース対策)
@app.route('/api/auth/verify-otp', methods=['POST'])
@limiter.limit("3 per 5 minutes")
def verify_otp():
...
// Express + express-rate-limit
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 10, // 最大10回
message: { error: 'Too many login attempts, please try again later' },
standardHeaders: true, // RateLimit-* ヘッダーを返す
legacyHeaders: false,
});
app.post('/api/auth/login', loginLimiter, loginHandler);
レスポンスヘッダーで残りリクエスト数を伝える(標準化: RFC 6585):
RateLimit-Limit: 10
RateLimit-Remaining: 7
RateLimit-Reset: 1680000000
Retry-After: 60 ← 制限超過時
API7: SSRF(Server Side Request Forgery)
サーバーに任意の URL へのリクエストを発行させ、内部ネットワークやクラウドメタデータ API を攻撃する。
攻撃フロー:
1. API が URL を受け取ってコンテンツを取得する機能を持つ
POST /api/fetch-preview {"url": "https://example.com/image.png"}
2. 攻撃者が内部リソースの URL を指定
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
→ クラウドの IAM 認証情報が返る
3. さらに内部サービスへのアクセス
{"url": "http://internal-db:5432/"}
{"url": "http://admin-panel.internal/"}
# 安全な URL フェッチ実装
import ipaddress
from urllib.parse import urlparse
import requests
ALLOWED_SCHEMES = {'https'} # http は禁止
BLOCKED_HOSTS = {'localhost', '127.0.0.1', '0.0.0.0'}
def is_safe_url(url: str) -> bool:
try:
parsed = urlparse(url)
except Exception:
return False
# スキーム検証
if parsed.scheme not in ALLOWED_SCHEMES:
return False
# ホスト検証
hostname = parsed.hostname
if not hostname or hostname in BLOCKED_HOSTS:
return False
# プライベート IP アドレス・ループバックを拒否
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
except ValueError:
pass # ホスト名の場合は IP アドレスではないので続行
return True
def fetch_url(url: str) -> bytes:
if not is_safe_url(url):
raise ValueError("URL is not allowed")
# リダイレクト追跡を無効化(リダイレクトで内部 URL に誘導される可能性)
response = requests.get(url, allow_redirects=False, timeout=5)
return response.content
追加の SSRF 対策:
・DNS rebinding 対策: URL 解決後の IP アドレスも検証する
・アウトバウンド通信を外部専用 Egress に限定(ネットワーク分離)
・クラウドメタデータ API へのアクセスをインスタンスレベルで制限
(AWS: IMDSv2 の強制で Token 必須に)
API5: Broken Function Level Authorization
管理者用エンドポイントの認可チェック漏れ。
# 危険: HTTP メソッドで機能を分けているだけで認可していない
@app.route('/api/v1/users/<int:user_id>', methods=['GET', 'DELETE'])
@jwt_required
def user_endpoint(user_id):
if request.method == 'GET':
return jsonify(User.query.get_or_404(user_id).to_dict())
elif request.method == 'DELETE':
# ← DELETE は管理者だけのはずなのに認可チェックがない
User.query.get_or_404(user_id).delete()
db.session.commit()
return '', 204
# 安全: メソッドごとに認可を明示
@app.route('/api/v1/users/<int:user_id>', methods=['GET'])
@jwt_required
def get_user(user_id):
...
@app.route('/api/v1/users/<int:user_id>', methods=['DELETE'])
@admin_required # ← 管理者専用デコレータ
def delete_user(user_id):
...
よくある見落とし:
・/api/v1/users → GET(通常ユーザー)
・/api/admin/users → DELETE(管理者)の URL 分離だけに頼るパターン
→ admin URL がドキュメント化されていなくても推測できる(/api/v2/admin, /api/internal 等)
→ 必ずコード内で認可チェックを実装する
GraphQL のセキュリティ
GraphQL は REST より柔軟な分、固有のセキュリティリスクがある。
Introspection の制限
// 本番環境では Introspection を無効化(スキーマの公開を防ぐ)
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production', // 本番は無効
});
Introspection が有効だと:
・スキーマ構造(全フィールド・型)が誰でも取得できる
・攻撃者がカスタムクエリを設計する材料になる
・InQL(Burp Extension)で自動的に攻撃クエリが生成される
クエリの深さ・複雑度の制限
// graphql-depth-limit + graphql-query-complexity
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5), // ネスト深さ上限
createComplexityLimitRule(1000, { // クエリ複雑度の上限
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});
クエリ深さ攻撃(Nested Query Attack):
query {
user {
friends {
friends {
friends { ← 無限ネストで DB を爆発的に負荷
friends { ... }
}
}
}
}
}
バッチリクエストの制限
// GraphQL はバッチクエリでレートリミットを回避できる
// [{ query: "query1" }, { query: "query2" }, ..., { query: "query1000" }]
// Apollo: バッチサイズを制限
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [{
requestDidStart() {
return {
didResolveOperation({ request }) {
if (Array.isArray(request) && request.length > 10) {
throw new Error('Batch size limit exceeded');
}
},
};
},
}],
});
REST API のセキュリティベストプラクティス
API バージョン管理と廃止ポリシー
API9: Improper Inventory Management
旧バージョン API(/api/v1/)が脆弱なまま残っている問題。
対策:
・/api/v1/, /api/v2/ など全バージョンに同じ認証・認可・バリデーションを適用
・廃止するバージョンは Deprecation ヘッダーで予告し、一定期間後に削除
・API インベントリを管理し、シャドウ API(非公式エンドポイント)を定期的に探索
# Deprecation ヘッダー(RFC 8594)
@app.route('/api/v1/users')
def get_users_v1():
response = make_response(jsonify(users))
response.headers['Deprecation'] = 'true'
response.headers['Sunset'] = 'Sat, 31 Dec 2026 23:59:59 GMT'
response.headers['Link'] = '</api/v2/users>; rel="successor-version"'
return response
レスポンスの情報漏洩を防ぐ
# 危険: DB エラーをそのまま返す
@app.errorhandler(Exception)
def handle_error(e):
return jsonify({'error': str(e)}), 500
# → "UNIQUE constraint failed: users.email" のような DB 情報が漏れる
# 安全: 詳細はログに記録し、クライアントには汎用メッセージのみ返す
import logging
@app.errorhandler(Exception)
def handle_error(e):
logging.error(f"Unhandled exception: {e}", exc_info=True)
return jsonify({'error': 'Internal server error'}), 500
# フィールドの選択的な公開(機密フィールドを除外)
class UserSchema(Schema):
id = fields.UUID()
display_name = fields.String()
email = fields.Email()
created_at = fields.DateTime()
# password_hash, mfa_secret などは定義しない → 自動的にシリアライズされない
@app.route('/api/v1/users/<uuid:user_id>')
def get_user(user_id):
user = User.query.get_or_404(user_id)
return UserSchema().dump(user) # スキーマで制御されたフィールドのみ返す
API キー管理
# API キーの安全な生成と保存
import secrets
import hashlib
def generate_api_key() -> tuple[str, str]:
"""API キーを生成し、(平文キー, ハッシュ)のペアを返す"""
raw_key = secrets.token_urlsafe(32) # 256bit のランダムキー
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
return raw_key, key_hash # 平文は1回だけユーザーに表示
# DB には key_hash のみ保存(平文は保存しない)
# 検証時: 受け取ったキーを SHA256 して DB の key_hash と比較
def verify_api_key(raw_key: str, stored_hash: str) -> bool:
computed = hashlib.sha256(raw_key.encode()).hexdigest()
return secrets.compare_digest(computed, stored_hash) # タイミング攻撃対策
SSRF を使ったクラウドメタデータ攻撃と IMDSv2
AWS の IMDS(Instance Metadata Service)攻撃:
# IMDSv1(脆弱): 単純な GET で IAM 認証情報が取れる
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# SSRF を悪用した攻撃:
POST /api/fetch-url
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role"}
→ Access Key / Secret Key / Session Token が返る → AWS リソースを自由に操作
# IMDSv2 の強制(Token 必須): SSRF では Token 取得ができない
# Terraform で IMDSv2 を強制
resource "aws_instance" "app" {
ami = "ami-xxx"
instance_type = "t3.micro"
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2 を強制
http_put_response_hop_limit = 1 # コンテナからの SSRF を制限
}
}
API セキュリティのチェックリスト
□ 全エンドポイントでオブジェクトレベルの認可検証(オーナーシップチェック)をしている
□ リクエストボディの更新可能フィールドをホワイトリスト化している(Mass Assignment 防止)
□ ログイン・OTP・SMS 送信エンドポイントにレートリミットを設定している
□ GraphQL の Introspection を本番環境で無効化している
□ GraphQL のクエリ深さ・複雑度に上限を設けている
□ URL フェッチ機能でプライベート IP・メタデータ URL をブロックしている(SSRF 対策)
□ 管理者機能に認可チェックを実装している(URL の難読化だけに頼らない)
□ エラーレスポンスに DB・スタックトレースの詳細を含めていない
□ API キーを DB にハッシュ化して保存している(平文保存しない)
□ 廃止バージョン API を Deprecation ヘッダーで告知し、スケジュール通りに削除している
□ AWS IMDSv2 を強制している(http_tokens = required)
□ レスポンスに含める機密フィールドをスキーマで制御している
参考文献
- OWASP API Security Top 10 2023(owasp.org/API-Security)— API 固有のリスクカテゴリと対策の公式ガイド
- PortSwigger Web Security Academy『API testing』— API の認証・認可・SSRF の実践ラボ
- PortSwigger Web Security Academy『Server-side request forgery(SSRF)』— SSRF の攻撃パターンと防止手法
- Dafydd Stuttard & Marcus Pinto『The Web Application Hacker’s Handbook』(Wiley, 2011)— API 攻撃の体系的解説
- AWS Security Best Practices『IMDSv2』— EC2 メタデータサービスのセキュリティ強化ガイド
- 1. 🌍Webアプリセキュリティ概要
- 2. 💉インジェクション攻撃(SQLi・コマンド・XXE)
- 3. 📜XSS(クロスサイトスクリプティング)・CSP
- 4. 🪤CSRF・Clickjacking・CORS
- 5. 🚪認証・認可の実装ミス(セッション・JWT・OAuth)
- 6. 🔌API セキュリティ(REST・GraphQL・OWASP API Top 10)
出典: OWASP API Security Top 10 2023 / The Web Application Hacker's Handbook(Stuttard & Pinto, 2011)/ PortSwigger Web Security Academy / AWS Security Best Practices