mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
27c6414ca5
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
160 lines
6.4 KiB
Python
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()
|