| name | detecting-broken-object-property-level-authorization |
| description | Detect and test for OWASP API3:2023 Broken Object Property Level Authorization vulnerabilities including excessive data exposure and mass assignment attacks. |
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","bopla","owasp-api3","mass-assignment","excessive-data-exposure","property-level-authorization","api-testing","penetration-testing"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
Detecting Broken Object Property Level Authorization
Overview
Broken Object Property Level Authorization (BOPLA), classified as API3:2023 in the OWASP API Security Top 10, combines two related vulnerability classes: Excessive Data Exposure (API returning more data than needed) and Mass Assignment (API accepting more data than intended). Even when APIs enforce object-level authorization correctly, they may fail to control which specific properties of an object a user can read or modify. Attackers exploit this by reading sensitive properties from API responses or injecting additional properties into request bodies to modify fields they should not have access to.
Prerequisites
- Target API with endpoints that return or accept object data
- API documentation or schema (OpenAPI spec preferred)
- Burp Suite or Postman for API request manipulation
- Multiple user accounts with different privilege levels
- Python 3.8+ with requests library for automated testing
- Authorization to perform security testing
Vulnerability Patterns
Excessive Data Exposure
The API returns object properties the client does not need:
{
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"name": "John Doe",
"ssn": "123-45-6789",
"salary": 95000,
"internal_notes": "VIP client",
"password_hash": "$2b$12...",
"role": "admin",
"created_by": "system_admin",
"credit_card_last4": "4242"
Mass Assignment
The API binds client-supplied data to internal object properties without filtering:
// Normal user update request
PUT /api/v1/users/123
Content-Type: application/json
{
"name": "John Updated",
"email": "new@example.com",
"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
}
Testing Methodology
"""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
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": [
, , , ,
, , , ,
, , , , ,
],
: [
, , , , ,
, , ,
]
}
MASS_ASSIGNMENT_FIELDS = [
(, ),
(, ),
(, ),
(, ),
(, ),
(, ),
(, ),
(, ),
(, [, , ]),
(, ),
(, ),
(, ),
]
():
.base_url = base_url.rstrip()
.auth_headers = auth_headers
.findings: [BOPLAFinding] = []
() -> [BOPLAFinding]:
findings = []
url =
:
response = requests.get(url, headers=.auth_headers, timeout=)
response.status_code != :
findings
data = response.json()
objects = data (data, ) [data]
(data, ) data:
objects = data[] (data[], ) [data[]]
obj objects[:]:
(obj, ):
response_fields = (._flatten_keys(obj))
unexpected_fields = response_fields - expected_fields
field_name unexpected_fields:
severity = ._classify_sensitivity(field_name)
severity:
finding = BOPLAFinding(
endpoint=endpoint,
method=,
vulnerability_type=,
severity=severity,
property_name=field_name,
details=
)
findings.append(finding)
.findings.append(finding)
(requests.exceptions.RequestException, json.JSONDecodeError):
findings
() -> [BOPLAFinding]:
findings = []
url =
original_data :
:
response = requests.get(url, headers=.auth_headers, timeout=)
response.status_code == :
original_data = response.json()
:
original_data = {}
(requests.exceptions.RequestException, json.JSONDecodeError):
original_data = {}
field_name, injected_value .MASS_ASSIGNMENT_FIELDS:
field_name original_data:
original_value = original_data[field_name]
original_value == injected_value:
test_data = deepcopy(original_data)
test_data[field_name] = injected_value
headers = {**.auth_headers, : }
:
method == :
response = requests.put(url, json=test_data,
headers=headers, timeout=)
method == :
response = requests.patch(url, json={field_name: injected_value},
headers=headers, timeout=)
method == :
response = requests.post(url, json=test_data,
headers=headers, timeout=)
response.status_code (, , ):
verify_response = requests.get(url, headers=.auth_headers, timeout=)
verify_response.status_code == :
updated_data = verify_response.json()
updated_data.get(field_name) == injected_value:
finding = BOPLAFinding(
endpoint=endpoint,
method=method,
vulnerability_type=,
severity= field_name [, , ]
,
property_name=field_name,
details=
)
findings.append(finding)
.findings.append(finding)
field_name original_data:
restore_data = {field_name: original_data[field_name]}
requests.patch(url, json=restore_data,
headers=headers, timeout=)
requests.exceptions.RequestException:
findings
() -> [BOPLAFinding]:
findings = []
url =
introspection =
:
response = requests.post(
url,
json={: introspection},
headers=.auth_headers,
timeout=
)
response.status_code == :
data = response.json()
data:
finding = BOPLAFinding(
endpoint=graphql_endpoint,
method=,
vulnerability_type=,
severity=,
property_name=,
details=
)
findings.append(finding)
.findings.append(finding)
requests.exceptions.RequestException:
findings
() -> []:
keys = []
key, value obj.items():
full_key = prefix key
keys.append(full_key)
(value, ):
keys.extend(._flatten_keys(value, full_key))
keys
() -> []:
lower_name = field_name.lower().split()[-]
severity, patterns .SENSITIVE_PROPERTY_PATTERNS.items():
pattern patterns:
pattern lower_name:
severity.upper()
() -> :
{
: (.findings),
: {
: ([f f .findings
f.vulnerability_type == ]),
: ([f f .findings
f.vulnerability_type == ]),
},
: {
: ([f f .findings f.severity == ]),
: ([f f .findings f.severity == ]),
: ([f f .findings f.severity == ]),
: ([f f .findings f.severity == ]),
},
: [
{
: f.endpoint,
: f.method,
: f.vulnerability_type,
: f.severity,
: f.property_name,
: f.details,
}
f .findings
]
}
Mitigation
class UserSerializer:
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}
WRITABLE_FIELDS = {'name', 'email', 'phone', 'avatar_url', 'preferences'}
def update_user(user_id, request_data, requesting_user):
safe_data = {k: v for k, v in request_data.items() if k in WRITABLE_FIELDS}
User.objects.(=user_id).update(**safe_data)
References