关于 detecting-broken-object-property-level-authorization
用于检测 API 对象属性级授权缺陷的安全技能,涵盖过度数据暴露和批量赋值。它为安全分析人员提供检测与监控验证流程。
主要功能
- 01检测过度数据暴露
- 02测试批量赋值风险
- 03依据 API schema 开展测试
- 04验证监控覆盖情况
适用场景
- 01调查 API 安全事件
- 02编写威胁狩猎查询
- 03评估 SOC 检测流程
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill detecting-broken-object-property-level-authorization安装后在 Claude Code 中输入/detecting-broken-object-property-level-authorization确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEreferences/api-reference.mdscripts/agent.py详细介绍
检测失效的对象属性级授权
概述
失效的对象属性级授权(BOPLA)在 OWASP API 安全 Top 10 中被归类为 API3:2023,它合并了两个相关的漏洞类别:过度数据暴露(API 返回的数据超出所需)和批量赋值(API 接受的数据超出预期)。即使 API 正确实施了对象级授权,它们也可能无法控制用户能读取或修改对象的哪些特定属性。攻击者利用这一点,从 API 响应中读取敏感属性,或向请求体中注入额外属性,以修改他们本不应有权访问的字段。
何时使用
- 在调查需要检测失效的对象属性级授权的安全事件时
- 在为该领域构建检测规则或威胁狩猎查询时
- 当 SOC 分析师需要针对此类分析的结构化流程时
- 在验证针对相关攻击技术的安全监控覆盖范围时
前提条件
- 目标 API 具有返回或接受对象数据的端点
- API 文档或架构(首选 OpenAPI 规范)
- 用于操纵 API 请求的 Burp Suite 或 Postman
- 具有不同权限级别的多个用户账户
- 用于自动化测试的 Python 3.8+ 及 requests 库
- 执行安全测试的授权
漏洞模式
过度数据暴露
API 返回客户端不需要的对象属性:
// GET /api/v1/users/123
// Response includes sensitive fields the UI doesn't display:
{
"id": 123,
"username": "john_doe",
"email": "[email protected]",
"name": "John Doe",
"ssn": "123-45-6789", // Sensitive - not needed by UI
"salary": 95000, // Sensitive - not needed by UI
"internal_notes": "VIP client", // Internal - should not be exposed
"password_hash": "$2b$12...", // Critical - never expose
"role": "admin", // May enable privilege discovery
"created_by": "system_admin", // Internal metadata
"credit_card_last4": "4242" // PCI compliance violation
}
批量赋值
API 在未过滤的情况下将客户端提供的数据绑定到内部对象属性:
// Normal user update request
PUT /api/v1/users/123
Content-Type: application/json
{
"name": "John Updated",
"email": "[email protected]",
"role": "admin", // Attacker-injected: privilege escalation
"is_verified": true, // Attacker-injected: bypass verification
"discount_rate": 100, // Attacker-injected: business logic abuse
"account_balance": 999999 // Attacker-injected: financial fraud
}
测试方法
#!/usr/bin/env python3
"""BOPLA Vulnerability Scanner
Tests APIs for Broken Object Property Level Authorization
including Excessive Data Exposure and Mass Assignment.
"""
import requests
import json
import sys
from typing import Dict, List, Optional, Set
from dataclasses import dataclass, field
from copy import deepcopy
@dataclass
class BOPLAFinding:
endpoint: str
method: str
vulnerability_type: str # "excessive_exposure" or "mass_assignment"
severity: str
property_name: str
details: str
class BOPLAScanner:
SENSITIVE_PROPERTY_PATTERNS = {
"critical": [
"password", "password_hash", "secret", "token", "api_key",
"private_key", "secret_key", "access_token", "refresh_token",
],
"high": [
"ssn", "social_security", "tax_id", "credit_card", "card_number",
"cvv", "bank_account", "routing_number",
],
"medium": [
"salary", "income", "internal_notes", "admin_notes",
"created_by", "modified_by", "ip_address", "session_id",
"role", "permissions", "is_admin", "is_superuser", "privilege",
],
"low": [
"phone", "address", "date_of_birth", "dob", "age",
"gender", "ethnicity", "religion",
]
}
MASS_ASSIGNMENT_FIELDS = [
("role", "admin"),
("is_admin", True),
("is_verified", True),
("is_active", True),
("email_verified", True),
("account_type", "premium"),
("discount_rate", 100),
("credit_limit", 999999),
("permissions", ["admin", "write", "delete"]),
("account_balance", 999999),
("subscription_tier", "enterprise"),
("rate_limit", 999999),
]
def __init__(self, base_url: str, auth_headers: Dict[str, str]):
self.base_url = base_url.rstrip('/')
self.auth_headers = auth_headers
self.findings: List[BOPLAFinding] = []
def test_excessive_data_exposure(self, endpoint: str,
expected_fields: Set[str]) -> List[BOPLAFinding]:
"""Test if API response contains more fields than expected."""
findings = []
url = f"{self.base_url}{endpoint}"
try:
response = requests.get(url, headers=self.auth_headers, timeout=10)
if response.status_code != 200:
return findings
data = response.json()
# Handle both single object and list responses
objects = data if isinstance(data, list) else [data]
if isinstance(data, dict) and "data" in data:
objects = data["data"] if isinstance(data["data"], list) else [data["data"]]
for obj in objects[:5]: # Check first 5 objects
if not isinstance(obj, dict):
continue
response_fields = set(self._flatten_keys(obj))
unexpected_fields = response_fields - expected_fields
for field_name in unexpected_fields:
severity = self._classify_sensitivity(field_name)
if severity:
finding = BOPLAFinding(
endpoint=endpoint,
method="GET",
vulnerability_type="excessive_exposure",
severity=severity,
property_name=field_name,
details=f"Unexpected sensitive field '{field_name}' in response"
)
findings.append(finding)
self.findings.append(finding)
except (requests.exceptions.RequestException, json.JSONDecodeError):
pass
return findings
def test_mass_assignment(self, endpoint: str, method: str = "PUT",
original_data: Optional[dict] = None) -> List[BOPLAFinding]:
"""Test if API accepts and processes additional injected properties."""
findings = []
url = f"{self.base_url}{endpoint}"
# First, get the current object state
if original_data is None:
try:
response = requests.get(url, headers=self.auth_headers, timeout=10)
if response.status_code == 200:
original_data = response.json()
else:
original_data = {}
except (requests.exceptions.RequestException, json.JSONDecodeError):
original_data = {}
# Test each mass assignment field
for field_name, injected_value in self.MASS_ASSIGNMENT_FIELDS:
if field_name in original_data:
# Field exists - test if we can modify it
original_value = original_data[field_name]
if original_value == injected_value:
continue # Already has this value
test_data = deepcopy(original_data)
test_data[field_name] = injected_value
headers = {**self.auth_headers, "Content-Type": "application/json"}
try:
if method == "PUT":
response = requests.put(url, json=test_data,
headers=headers, timeout=10)
elif method == "PATCH":
response = requests.patch(url, json={field_name: injected_value},
headers=headers, timeout=10)
elif method == "POST":
response = requests.post(url, json=test_data,
headers=headers, timeout=10)
if response.status_code in (200, 201, 204):
# Verify the field was actually modified
verify_response = requests.get(url, headers=self.auth_headers, timeout=10)
if verify_response.status_code == 200:
updated_data = verify_response.json()
if updated_data.get(field_name) == injected_value:
finding = BOPLAFinding(
endpoint=endpoint,
method=method,
vulnerability_type="mass_assignment",
severity="CRITICAL" if field_name in ["role", "is_admin", "permissions"]
else "HIGH",
property_name=field_name,
details=f"Successfully injected '{field_name}={injected_value}'"
)
findings.append(finding)
self.findings.append(finding)
# Restore original value if possible
if field_name in original_data:
restore_data = {field_name: original_data[field_name]}
requests.patch(url, json=restore_data,
headers=headers, timeout=10)
except requests.exceptions.RequestException:
continue
return findings
def test_graphql_property_exposure(self, graphql_endpoint: str,
query: str) -> List[BOPLAFinding]:
"""Test GraphQL APIs for property-level authorization issues."""
findings = []
url = f"{self.base_url}{graphql_endpoint}"
# Introspection query to discover available fields
introspection = """
{
__schema {
types {
name
fields {
name
type { name kind }
}
}
}
}
"""
try:
response = requests.post(
url,
json={"query": introspection},
headers=self.auth_headers,
timeout=10
)
if response.status_code == 200:
data = response.json()
if "errors" not in data:
finding = BOPLAFinding(
endpoint=graphql_endpoint,
method="POST",
vulnerability_type="excessive_exposure",
severity="MEDIUM",
property_name="__schema",
details="GraphQL introspection enabled - full schema exposed"
)
findings.append(finding)
self.findings.append(finding)
except requests.exceptions.RequestException:
pass
return findings
def _flatten_keys(self, obj: dict, prefix: str = "") -> List[str]:
"""Recursively flatten nested dictionary keys."""
keys = []
for key, value in obj.items():
full_key = f"{prefix}.{key}" if prefix else key
keys.append(full_key)
if isinstance(value, dict):
keys.extend(self._flatten_keys(value, full_key))
return keys
def _classify_sensitivity(self, field_name: str) -> Optional[str]:
"""Classify the sensitivity level of a field name."""
lower_name = field_name.lower().split('.')[-1]
for severity, patterns in self.SENSITIVE_PROPERTY_PATTERNS.items():
for pattern in patterns:
if pattern in lower_name:
return severity.upper()
return None
def generate_report(self) -> dict:
return {
"total_findings": len(self.findings),
"by_type": {
"excessive_exposure": len([f for f in self.findings
if f.vulnerability_type == "excessive_exposure"]),
"mass_assignment": len([f for f in self.findings
if f.vulnerability_type == "mass_assignment"]),
},
"by_severity": {
"CRITICAL": len([f for f in self.findings if f.severity == "CRITICAL"]),
"HIGH": len([f for f in self.findings if f.severity == "HIGH"]),
"MEDIUM": len([f for f in self.findings if f.severity == "MEDIUM"]),
"LOW": len([f for f in self.findings if f.severity == "LOW"]),
},
"findings": [
{
"endpoint": f.endpoint,
"method": f.method,
"type": f.vulnerability_type,
"severity": f.severity,
"property": f.property_name,
"details": f.details,
}
for f in self.findings
]
}
缓解措施
# Server-side: Explicit property allowlists
class UserSerializer:
# Only expose these fields - never use to_json() or to_dict()
PUBLIC_FIELDS = ['id', 'username', 'name', 'avatar_url']
OWNER_FIELDS = PUBLIC_FIELDS + ['email', 'phone', 'preferences']
ADMIN_FIELDS = OWNER_FIELDS + ['role', 'created_at', 'last_login']
def serialize(self, user, requesting_user):
if requesting_user.is_admin:
fields = self.ADMIN_FIELDS
elif requesting_user.id == user.id:
fields = self.OWNER_FIELDS
else:
fields = self.PUBLIC_FIELDS
return {field: getattr(user, field) for field in fields}
# Mass assignment protection - explicit allowlist for writable fields
WRITABLE_FIELDS = {'name', 'email', 'phone', 'avatar_url', 'preferences'}
def update_user(user_id, request_data, requesting_user):
# Filter out any fields not in the allowlist
safe_data = {k: v for k, v in request_data.items() if k in WRITABLE_FIELDS}
# Apply updates only with safe data
User.objects.filter(id=user_id).update(**safe_data)
参考资料
- OWASP API3:2023: https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
- Salt Security BOPLA 分析: https://salt.security/blog/api3-2023-broken-object-property-level-authorization
- Wallarm BOPLA 指南: https://lab.wallarm.com/api32023-broken-object-property-level-authorization/
- API Security News BOPLA: https://apisecurity.io/owasp-api-security-top-10/api3-2023-broken-object-property-level-authorization/
- CloudDefense BOPLA: https://www.clouddefense.ai/owasp/2023/3
评论
支持 Markdown