Files
mukul975 27c6414ca5 Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills
Complete skill folder anatomy across all cybersecurity skills:
- scripts/agent.py: 80-150 line Python agents using real libraries (impacket,
  boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.)
- references/api-reference.md: real API documentation with method signatures
- LICENSE: MIT license for all skill folders
2026-03-10 21:02:12 +01:00

160 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""BOPLA vulnerability scanner for OWASP API3:2023 Broken Object Property Level Authorization.
Tests APIs for excessive data exposure and mass assignment vulnerabilities
by comparing responses against expected field sets and injecting extra properties.
"""
import argparse
import json
import sys
from copy import deepcopy
try:
import requests
except ImportError:
print("Install requests: pip install requests")
sys.exit(1)
SENSITIVE_PATTERNS = {
"critical": ["password", "password_hash", "secret", "token", "api_key",
"private_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", "role", "permissions",
"is_admin", "is_superuser", "session_id", "ip_address"],
"low": ["phone", "address", "date_of_birth", "dob", "gender"]
}
MASS_ASSIGNMENT_PAYLOADS = [
("role", "admin"), ("is_admin", True), ("is_verified", True),
("email_verified", True), ("account_type", "premium"),
("discount_rate", 100), ("permissions", ["admin", "write", "delete"]),
("account_balance", 999999), ("subscription_tier", "enterprise"),
]
def classify_field(field_name):
lower = field_name.lower().split(".")[-1]
for severity, patterns in SENSITIVE_PATTERNS.items():
for p in patterns:
if p in lower:
return severity.upper()
return None
def flatten_keys(obj, prefix=""):
keys = []
for k, v in obj.items():
full = f"{prefix}.{k}" if prefix else k
keys.append(full)
if isinstance(v, dict):
keys.extend(flatten_keys(v, full))
return keys
def test_excessive_exposure(base_url, endpoint, expected_fields, headers):
findings = []
url = f"{base_url.rstrip('/')}{endpoint}"
try:
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code != 200:
return findings
data = resp.json()
objects = data if isinstance(data, list) else [data]
if isinstance(data, dict) and "data" in data:
inner = data["data"]
objects = inner if isinstance(inner, list) else [inner]
for obj in objects[:5]:
if not isinstance(obj, dict):
continue
response_fields = set(flatten_keys(obj))
unexpected = response_fields - set(expected_fields)
for field in unexpected:
sev = classify_field(field)
if sev:
findings.append({
"endpoint": endpoint, "method": "GET",
"type": "excessive_exposure", "severity": sev,
"property": field,
"detail": f"Unexpected sensitive field '{field}' in response"
})
except (requests.RequestException, json.JSONDecodeError) as e:
findings.append({"error": str(e)})
return findings
def test_mass_assignment(base_url, endpoint, headers, method="PUT"):
findings = []
url = f"{base_url.rstrip('/')}{endpoint}"
try:
original = requests.get(url, headers=headers, timeout=10)
original_data = original.json() if original.status_code == 200 else {}
except (requests.RequestException, json.JSONDecodeError):
original_data = {}
for field_name, injected_value in MASS_ASSIGNMENT_PAYLOADS:
if original_data.get(field_name) == injected_value:
continue
test_data = deepcopy(original_data)
test_data[field_name] = injected_value
try:
h = {**headers, "Content-Type": "application/json"}
if method == "PUT":
resp = requests.put(url, json=test_data, headers=h, timeout=10)
elif method == "PATCH":
resp = requests.patch(url, json={field_name: injected_value}, headers=h, timeout=10)
else:
resp = requests.post(url, json=test_data, headers=h, timeout=10)
if resp.status_code in (200, 201, 204):
verify = requests.get(url, headers=headers, timeout=10)
if verify.status_code == 200 and verify.json().get(field_name) == injected_value:
sev = "CRITICAL" if field_name in ("role", "is_admin", "permissions") else "HIGH"
findings.append({
"endpoint": endpoint, "method": method,
"type": "mass_assignment", "severity": sev,
"property": field_name,
"detail": f"Injected {field_name}={injected_value} accepted"
})
if field_name in original_data:
requests.patch(url, json={field_name: original_data[field_name]},
headers=h, timeout=10)
except requests.RequestException:
continue
return findings
def main():
parser = argparse.ArgumentParser(description="BOPLA Vulnerability Scanner (OWASP API3:2023)")
parser.add_argument("--base-url", required=True, help="API base URL")
parser.add_argument("--endpoint", required=True, help="Endpoint to test (e.g. /api/v1/users/1)")
parser.add_argument("--token", help="Bearer token for Authorization header")
parser.add_argument("--expected-fields", nargs="+", default=[], help="Expected response fields")
parser.add_argument("--test", choices=["exposure", "mass_assignment", "both"], default="both")
parser.add_argument("--method", choices=["PUT", "PATCH", "POST"], default="PUT")
args = parser.parse_args()
headers = {}
if args.token:
headers["Authorization"] = f"Bearer {args.token}"
results = {"endpoint": args.endpoint, "findings": []}
if args.test in ("exposure", "both"):
results["findings"].extend(
test_excessive_exposure(args.base_url, args.endpoint, args.expected_fields, headers))
if args.test in ("mass_assignment", "both"):
results["findings"].extend(
test_mass_assignment(args.base_url, args.endpoint, headers, args.method))
results["total_findings"] = len(results["findings"])
results["by_severity"] = {}
for f in results["findings"]:
sev = f.get("severity", "UNKNOWN")
results["by_severity"][sev] = results["by_severity"].get(sev, 0) + 1
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()