broken-access-control — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited broken-access-control (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The #1 OWASP risk for two consecutive cycles. Found in 3.74% of applications with over 1.8 million occurrences and 32,654 CVEs. Access control enforces that users cannot act outside their intended permissions. Failures lead to unauthorized data disclosure, modification, or destruction.
For deep reference on all CWEs and remediation patterns, see references/bac-detail.md.| Sub-category | Description | Key CWEs |
|---|---|---|
| Missing Authorization | Routes/endpoints with no auth guard | CWE-862, CWE-284 |
| IDOR | Resource ownership not verified | CWE-639, CWE-285 |
| CORS Misconfiguration | Wildcard or overly-permissive origins | CWE-284 |
| SSRF | Outbound requests to internal/private IPs | CWE-918 |
| CSRF | State-changing forms missing token validation | CWE-352 |
| Privilege Escalation | Users accessing higher-privilege roles | CWE-269 |
| JWT/Session Manipulation | Metadata tampering, session fixation | CWE-287 |
FastAPI:
# List all routes and their dependencies
for route in app.routes:
print(route.path, route.methods, route.dependencies)Flask:
# Print all registered endpoints
for rule in app.url_map.iter_rules():
print(rule.endpoint, rule.methods, rule.rule)Flag any route that:
FastAPI — look for missing `Depends()`:
# VULNERABLE — no auth check
@app.get("/users/{user_id}/data")
async def get_user_data(user_id: int):
return db.query(UserData).filter_by(owner_id=user_id).all()
# SECURE — requires authenticated user
@app.get("/users/{user_id}/data")
async def get_user_data(user_id: int, current_user: User = Depends(get_current_user)):
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Forbidden")
return db.query(UserData).filter_by(owner_id=user_id).all()Flask — look for missing `@login_required`:
# VULNERABLE
@app.route("/dashboard")
def dashboard():
return render_template("dashboard.html")
# SECURE
@app.route("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html")IDOR is the most common BAC failure. Every query that fetches a user-owned resource must compare resource.owner_id against current_user.id.
# VULNERABLE IDOR — attacker can request any document_id
@app.get("/documents/{doc_id}")
async def get_doc(doc_id: int, user=Depends(get_current_user)):
return db.get(Document, doc_id) # No ownership check!
# SECURE
@app.get("/documents/{doc_id}")
async def get_doc(doc_id: int, user=Depends(get_current_user)):
doc = db.get(Document, doc_id)
if not doc or doc.owner_id != user.id:
raise HTTPException(status_code=404) # 404 preferred over 403 (avoids enumeration)
return docTest vector: Authenticate as User A, record a resource ID, then authenticate as User B and request that same ID. If you get data — it's an IDOR.
FastAPI:
# VULNERABLE — wildcard origin
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
# SECURE — explicit allowlist
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.yourdomain.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)Flask:
# VULNERABLE
CORS(app, origins="*")
# SECURE
CORS(app, origins=["https://app.yourdomain.com"], supports_credentials=True)⚠️allow_origins=["*"]combined withallow_credentials=Trueis doubly dangerous — most browsers block this, but it signals deep misconfiguration.
Check any code that makes outbound HTTP requests with user-controlled URLs.
import ipaddress, re
PRIVATE_RANGES = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # AWS metadata
]
def is_safe_url(url: str) -> bool:
from urllib.parse import urlparse
import socket
try:
host = urlparse(url).hostname
ip = ipaddress.ip_address(socket.gethostbyname(host))
return not any(ip in net for net in PRIVATE_RANGES)
except Exception:
return False
# In your endpoint:
@app.post("/fetch")
async def fetch_url(url: str, user=Depends(get_current_user)):
if not is_safe_url(url):
raise HTTPException(status_code=400, detail="URL not allowed")
return await httpx.get(url)Test vectors: http://169.254.169.254/latest/meta-data/ (AWS metadata), http://localhost:6379 (Redis), http://10.0.0.1/admin
FastAPI APIs using JWT/Bearer tokens are generally CSRF-safe. Flask apps using session cookies need explicit protection.
# Flask — verify Flask-WTF is installed and CSRF is enabled
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# All state-changing forms must include {{ form.hidden_tag() }} or {{ csrf_token() }}Check that WTF_CSRF_ENABLED = True in config (it is by default, but verify it hasn't been disabled).
# Verify JWT expiry is set (short-lived: 15 min–1 hr)
jwt.encode({"sub": user_id, "exp": datetime.utcnow() + timedelta(minutes=30)}, SECRET_KEY)
# Flask — verify session security flags
app.config["SESSION_COOKIE_SECURE"] = True # HTTPS only
app.config["SESSION_COOKIE_HTTPONLY"] = True # No JS access
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # CSRF mitigation
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=2)Run through this for any Python web app:
resource.owner_id == current_user.id*exp claims; sessions have SECURE, HTTPONLY, SAMESITE flags| Tool | What it catches | Command |
|---|---|---|
| Bandit | SSRF patterns, hardcoded secrets, dangerous functions | bandit -r . -t B105,B106,B107,B501,B601 |
| OWASP ZAP | IDOR, missing auth at runtime, CORS | zap-baseline.py -t http://localhost:8000 |
| truffleHog | Leaked tokens/keys in git history | trufflehog git file://. --only-verified |
| CWE | Name | Typical Pattern |
|---|---|---|
| CWE-284 | Improper Access Control | Route accessible without any auth |
| CWE-285 | Improper Authorization | Auth present but role/ownership not checked |
| CWE-352 | CSRF | State-changing endpoint accepts cookie auth, no CSRF token |
| CWE-639 | Auth Bypass via User-Controlled Key | IDOR — GET /orders/{id} with no ownership check |
| CWE-862 | Missing Authorization | No @login_required or Depends(get_current_user) |
| CWE-918 | SSRF | requests.get(user_input_url) without validation |
| CWE-200 | Sensitive Info Exposure | Unauthorized user receives another's PII/data |
references/bac-detail.md — Expanded CWE descriptions, more attack examples, compliance mapping~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.