# API Security Review

> Review a REST or GraphQL API for security vulnerabilities — every item of the OWASP API Top 10 2023 (BOLA, broken auth, mass assignment, rate limiting, SSRF, GraphQL depth/complexity), JWT/OAuth pitfalls, CORS misconfig, and security headers. Outputs severity-rated findings with curl test snippets and code-level fixes. Use to vet an OpenAPI spec, audit an endpoint diff, or pre-deploy a service.

- Skill: `viprasol-tech/api-security-review` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add viprasol-tech/api-security-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/viprasol-tech/api-security-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Viprasol-Tech (https://skillmd.com/u/viprasol-tech)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/viprasol-tech/api-security-review

---


# API Security Review

A disciplined, repeatable methodology for reviewing a **REST or GraphQL API**
against the **OWASP API Security Top 10 (2023)** — Broken Object Level
Authorization, Broken Authentication, Broken Object Property Level
Authorization, Unrestricted Resource Consumption, Broken Function Level
Authorization, Unrestricted Access to Sensitive Business Flows, Server-Side
Request Forgery, Security Misconfiguration, Improper Inventory Management, and
Unsafe Consumption of APIs — plus the cross-cutting auth, CORS, and GraphQL
checks that account for most real-world breaches. **BOLA alone is roughly 40%
of observed API attacks.**

The skill takes an OpenAPI spec, an endpoint diff, a request/response sample, or
a chunk of API source, and returns severity-rated findings with **curl test
snippets** and **code-level fixes**, plus a **coverage matrix** so callers see
what was actually checked.

> **Honest scope & limits.** I cannot hit a live server. I reason over what you
> provide — an OpenAPI spec, source code, request/response samples, or
> descriptions. The output is **leads + a hardening plan**, not a substitute for
> a professional penetration test or a live DAST run. Not legal or compliance
> advice; PCI / HIPAA / SOC 2 attestation requires qualified auditors.

---

## When to Activate

Activate when the user:

- Shares an **OpenAPI / Swagger spec** (`openapi.yaml`, `swagger.json`) and asks
  for a security pass.
- Pastes **REST or GraphQL endpoint source code** (FastAPI, Flask, Express,
  NestJS, Rails, Django REST Framework, Spring, Go) and asks if it's safe to
  ship.
- Drops a **pull-request diff** touching API code, routes, auth middleware, an
  API gateway config, or CORS settings.
- Shares a **request/response sample** (curl, HAR, Postman) and asks "is this
  API secure?" or "what's leaking here?".
- Mentions any of: **OWASP API Top 10, BOLA, broken auth, mass assignment, JWT,
  OAuth, OIDC, CORS, SSRF, rate limit, GraphQL depth/complexity, security
  headers**.

---

## Step 1: Intake & Scope

Establish the review's scope before saying anything about safety.

1. **What are we reviewing?**
   - **(A) An OpenAPI / Swagger spec** — full or excerpt.
   - **(B) Endpoint source code** — one route, a module, or the whole service.
   - **(C) A PR / diff** touching API code.
   - **(D) A request/response sample** — curl, HAR, browser network capture.
   - **(E) An API gateway config** — Kong, Apigee, AWS API Gateway, NGINX, Envoy.
2. **Stack.** Language & framework: FastAPI, Flask, Django REST Framework,
   Express, NestJS, Hapi, Rails, Spring (Boot/MVC), ASP.NET, Go (gin/echo/chi).
   Different idioms hide different bugs.
3. **Auth model.** JWT (HS / RS / ES) · OAuth 2.0 / OIDC (Authorization Code +
   PKCE, Client Credentials, Implicit — deprecated) · session cookies · API
   keys · mTLS · signed requests. Where is auth enforced — gateway, middleware,
   or per-route?
4. **REST · GraphQL · both?** GraphQL needs extra checks (introspection, depth,
   complexity, per-resolver authz).
5. **Exposure.** Public internet · partner-only (API key + IP allowlist) ·
   internal (assumed trusted — verify) · machine-to-machine.
6. **What can you share?** The spec? The route handler? A few sample requests?
   The auth middleware? Without artefacts, ask before guessing.

Do not proceed until you know **what** is in scope, **which stack**, and **the
auth model**.

---

## Step 2: Severity Model

Rate every finding by **blast radius × likelihood**.

| Severity | Meaning |
|---|---|
| 🔴 **Critical** | Full account or data takeover, mass PII leak, RCE, privilege escalation. Block deploy. |
| 🟠 **High** | Auth bypass on a single account, partial data leak, exploitable BOLA on a sensitive resource. |
| 🟡 **Medium** | DoS via missing rate limit, info leak (stack trace, version banner), CSRF on a non-critical flow. |
| 🟢 **Low** | Hardening gap on top of a strong default (missing one security header, weak cipher coexisting with a strong default). |
| ℹ️ **Info** | Best-practice nits, documentation gaps, naming hygiene. |

---

## Step 3: OWASP API Top 10 (2023) — Review Passes

Use the exact IDs and names. For each: *what it is · how to spot it · exploit
scenario · fix.* Cite each finding in your report by its OWASP ID.

### API1:2023 Broken Object Level Authorization

- **What it is.** An endpoint operates on `/orders/{id}`, `/users/{userId}`,
  `/files/{fileId}` and authenticates the caller — but never checks that the
  caller **owns** that object. ~**40% of all API attacks**. Unchanged from 2019
  because it remains the dominant API bug class.
- **Spot it.** Any path with `{id}` / `{uuid}` / numeric IDs where the handler
  loads the object by id alone, with no `WHERE owner_id = caller` filter and no
  explicit ownership check. Sequential or guessable IDs (incrementing integers)
  amplify it.
- **Exploit.** Alice's token, Bob's `userId` in the URL → Bob's data.
- **Fix.** Explicit owner check **at the handler**, deny-by-default:

  ```python
  # FastAPI / SQLAlchemy
  obj = db.get(Order, order_id)
  if obj is None or obj.owner_id != request.user.id:
      raise HTTPException(status_code=404)  # 404, not 403, to avoid id enumeration
  ```

  Or push it into the query: `Order.objects.filter(id=order_id, owner_id=user.id).first()`.
  Prefer **opaque, unguessable IDs** (UUIDv4) over sequential integers so
  enumeration is harder even if a check is missed.

### API2:2023 Broken Authentication

- **What it is.** Weak login or token validation lets attackers impersonate
  users — credential stuffing, brute force, predictable session IDs, weak
  password reset, and JWT/OAuth misuse.
- **JWT pitfalls (check every JWT system).**
  - `alg: none` accepted by the server.
  - **Algorithm confusion** — the server uses HMAC verify with the RSA *public*
    key as the secret (attacker signs an HS256 token with the public key, server
    accepts it).
  - Weak HS256 secret (dictionary-crackable).
  - No `iss`, `aud`, `exp` verification.
  - Long-lived tokens with no revocation list and no rotation.
  - Sensitive PII inside JWT claims (it's returned to the client).
  - JWT used as both session and authorization bearer with **no scope check**.
- **OAuth 2.0 / OIDC.**
  - Authorization Code flow **without PKCE** for public clients (SPAs, mobile).
  - Open-redirect on `redirect_uri` (always allowlist exact URIs).
  - **Implicit flow** still in use — deprecated.
  - Missing `state` / `nonce` → CSRF on the auth code grant.
  - Scope check missing at the resource server.
- **Sessions / cookies.** No `Secure`, `HttpOnly`, `SameSite=Lax|Strict`. Session
  ID in the URL. No re-auth for sensitive operations (password change, email
  change, payment).
- **Spot it.** `jwt.decode(token, key)` with no `algorithms=` allowlist;
  `verify_signature=False`; no `aud`/`iss` arguments; HS256 in a public-client
  setup; cookie set without `Secure`/`HttpOnly`/`SameSite`.
- **Fix (PyJWT).**

  ```python
  import jwt

  payload = jwt.decode(
      token,
      public_key,
      algorithms=["RS256"],          # pin the algorithm — block alg=none + confusion
      audience="my-api",             # verify aud
      issuer="https://auth.example", # verify iss
      options={"require": ["exp", "iat", "iss", "aud"]},
  )
  ```

  Use **short-lived access tokens** (5–15 min) + refresh tokens with rotation,
  add a token revocation list (`jti`), and never put PII in claims you wouldn't
  log.

### API3:2023 Broken Object Property Level Authorization

- **What it is.** Two sub-classes merged: **excessive data exposure** (the
  response includes properties the caller shouldn't see — `password_hash`,
  `internal_flags`, `mfa_secret`) and **mass assignment** (the request body
  writes properties the caller shouldn't touch — `isAdmin`, `balance`, `tenant_id`).
- **Spot it.** `return user.__dict__` / `return user.to_dict()` / serializing the
  whole ORM row. `User(**request.json)` / `user.update(**body)` / Rails
  `user.update_attributes(params[:user])` without strong params. No
  request/response schema with an explicit allowlist.
- **Exploit.** `PATCH /users/me` with `{"isAdmin": true, "tenantId": 42}` and
  the user is now an admin in someone else's tenant.
- **Fix.** Explicit allowlist schemas on **input and output**:

  ```python
  # Pydantic — explicit, allow-list, no extras
  class UserUpdateIn(BaseModel):
      display_name: str | None = None
      email: EmailStr | None = None
      model_config = ConfigDict(extra="forbid")  # reject unknown fields

  class UserOut(BaseModel):
      id: UUID
      display_name: str
      email: EmailStr
      # NOTE: no password_hash, no is_admin, no internal flags
  ```

  ```typescript
  // Zod — strict() rejects extra keys
  const UserUpdateIn = z.object({
    displayName: z.string().min(1).max(80).optional(),
    email: z.string().email().optional(),
  }).strict();
  ```

  Never return the raw ORM row. Map to a response schema and `exclude` /
  `omit` sensitive fields by default.

### API4:2023 Unrestricted Resource Consumption

- **What it is.** Renamed from *Lack of Rate Limiting*. No rate limit, no
  payload size cap, no pagination cap, unbounded queries → DoS or cost
  blow-up (DB, LLM, third-party APIs).
- **Pragmatic checklist.**
  - **Token-keyed > IP-keyed.** NAT and CDNs flatten IPs; token-keyed limits
    track the real principal.
  - Separate buckets: **per-user, per-IP, per-endpoint, per-tenant**.
  - **Payload size cap** (HTTP body), **JSON depth** and **array-length** caps.
  - **Pagination:** enforce `limit ≤ 100`; never accept arbitrary `limit`.
  - Long-running **endpoint timeouts**.
  - **Cost budgets** on expensive resources (DB scans, LLM calls, third-party
    metering).
- **Spot it.** No middleware/limiter on routes. `limit = int(request.args["limit"])`
  with no max. No body-size middleware. Unlimited file uploads. Heavy joins
  with no pagination.
- **Fix.**

  ```python
  # FastAPI + slowapi — per-user token-keyed limiter
  from slowapi import Limiter
  from slowapi.util import get_remote_address

  def key_by_user(request):
      return request.state.user.id if hasattr(request.state, "user") else get_remote_address(request)

  limiter = Limiter(key_func=key_by_user, default_limits=["60/minute"])

  @app.get("/search")
  @limiter.limit("20/minute")
  def search(q: str, limit: int = Query(20, ge=1, le=100)):  # cap pagination
      ...
  ```

  ```javascript
  // Express + express-rate-limit, per-user
  import rateLimit from "express-rate-limit";
  app.use(express.json({ limit: "100kb" }));        // body cap
  app.use("/api/", rateLimit({
    windowMs: 60_000, max: 60,
    keyGenerator: (req) => req.user?.id || req.ip,  // token-keyed when possible
  }));
  ```

### API5:2023 Broken Function Level Authorization

- **What it is.** Admin / privileged functions are reachable by non-admins —
  the URL is "secret", the role check lives in the UI, or the check is on the
  wrong route. `POST /admin/users` accepts a normal user token.
- **Spot it.** No route-level role check; `if user.is_admin` only in the
  frontend; admin endpoints sharing the same router as user endpoints with no
  separate guard; "hidden" admin URLs that nonetheless reach the handler.
- **Exploit.** A regular user calls `POST /admin/users` and creates an admin
  account, or calls `DELETE /users/{id}` and erases anyone.
- **Fix.** **Deny by default**, allowlist roles, enforce at the route:

  ```python
  # FastAPI dependency-injected guard, applied per-route
  def require_admin(user: User = Depends(current_user)) -> User:
      if "admin" not in user.roles:
          raise HTTPException(status_code=403)
      return user

  @router.post("/admin/users", dependencies=[Depends(require_admin)])
  def create_user(...): ...
  ```

  Make the guard the **default**, then explicitly mark public routes — not the
  other way around. Cover `DELETE` / `PUT` / `PATCH` on every admin object.

### API6:2023 Unrestricted Access to Sensitive Business Flows

- **What it is.** New in 2023. Automation abuses the **business logic itself** —
  buy-all-tickets bots, reward farming, coupon stacking, mass account creation,
  mass password reset, mass follow/transfer/withdraw. Each individual request is
  "legal"; the volume is the abuse.
- **Sensitive flows to protect.** Account creation, password reset, login,
  coupon/promo redemption, purchase, review/comment, follow, transfer,
  withdraw, refer-a-friend rewards.
- **Spot it.** A `POST /signup` with no CAPTCHA, no device-fingerprint, no
  velocity check, no business-logic cap. A coupon endpoint with no per-user
  limit. A withdraw endpoint with no daily cap.
- **Fix.** Defense-in-depth at the **business layer**, not just rate limit:
  - **CAPTCHA** (hCaptcha, Turnstile) on high-value flows.
  - **Device fingerprint** + behavioural anomaly detection.
  - **Velocity checks** (per-user, per-IP, per-tenant, per-device).
  - **Hard business-logic caps** ("max 1 coupon per account per day", "max 3
    password resets per hour per email", "max $X withdrawn per day").

### API7:2023 Server-Side Request Forgery

- **What it is.** New in 2023. A URL or hostname comes from the user, and the
  server fetches it. Attackers point it at internal services — cloud metadata
  (`169.254.169.254`), internal admin panels, internal databases, neighbour
  pods.
- **Spot it.** Webhook subscription endpoints, "import from URL", PDF/HTML
  renderers, image proxies, link-preview generators, RSS importers.
- **Exploit.** `POST /webhooks/fetch {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}`
  → AWS IAM creds leaked back in the response.
- **Fix.** Strict URL allowlist; resolve DNS **before** fetch; reject private/
  link-local/loopback ranges; pin to HTTP(S); use a separate egress network if
  possible.

  ```python
  import ipaddress, socket
  from urllib.parse import urlparse

  BLOCKED_NETS = [ipaddress.ip_network(n) for n in (
      "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
      "127.0.0.0/8", "169.254.0.0/16",
      "fc00::/7", "::1/128", "fe80::/10",
  )]

  def safe_url(url: str) -> str:
      u = urlparse(url)
      if u.scheme not in ("http", "https"):
          raise ValueError("scheme not allowed")
      ip = ipaddress.ip_address(socket.gethostbyname(u.hostname))
      if any(ip in net for net in BLOCKED_NETS):
          raise ValueError("private/loopback/link-local not allowed")
      return url
  ```

  Bonus: defend against **DNS rebinding** — resolve once, fetch by the resolved
  IP (with the original Host header), so the second resolution can't flip to a
  private address after the check.

### API8:2023 Security Misconfiguration

- **What it is.** Default credentials, verbose errors, missing TLS, open admin
  panels, debug mode in production, missing security headers, permissive CORS,
  loose content-type handling.
- **Spot it.** `DEBUG = True`, `app.run(debug=True)`, stack traces in
  production responses, `Server: gunicorn/20.x` / `X-Powered-By: Express`,
  missing HSTS / CSP / `X-Content-Type-Options`, `Access-Control-Allow-Origin: *`
  with credentials.
- **Fix.** A baseline header set, strict content-type, generic error pages:

  ```python
  @app.middleware("http")
  async def security_headers(request, call_next):
      resp = await call_next(request)
      resp.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
      resp.headers["X-Content-Type-Options"] = "nosniff"
      resp.headers["Referrer-Policy"] = "no-referrer"
      resp.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
      resp.headers["Permissions-Policy"] = "interest-cohort=()"
      return resp
  ```

  TLS 1.2+ only, modern ciphers, no weak fallbacks. **Generic** error
  responses to clients; rich logs to the server side. Strict content-type
  check — reject anything not `application/json` on JSON endpoints.

### API9:2023 Improper Inventory Management

- **What it is.** Renamed from *Improper Asset Management*. Forgotten v1 /
  staging / test / `internal` endpoints with **weaker auth** still reachable.
  Old API versions kept running for "compatibility" with no deprecation plan.
  Undocumented endpoints that the security team doesn't know exist.
- **Spot it.** Routes labelled `v0`, `v1`, `legacy`, `internal`, `debug`,
  `staging` reachable from the public domain. No OpenAPI spec — or one that
  drifts from reality. No `Sunset` / `Deprecation` headers on retiring
  endpoints.
- **Fix.** **OpenAPI is the source of truth.** Generate it from code (FastAPI,
  NestJS, drf-spectacular) so it can't drift. Catalogue every endpoint, every
  version, every environment. Send **`Sunset`** and **`Deprecation`** headers
  on retiring versions and put a hard kill-date on them. Block `internal` /
  `debug` / `admin` paths at the edge (gateway/WAF) unless explicitly allowed.

### API10:2023 Unsafe Consumption of APIs

- **What it is.** New in 2023. Your API **trusts** a third-party API's response
  without validation — and that response can be hostile (compromised vendor,
  malicious user-controlled webhook, untrusted SaaS). Subsumes much of the old
  "injection" class.
- **Spot it.** `requests.get(vendor_url).json()` shoved straight into your DB
  or templated into another query. Deserializing untrusted blobs (`pickle`,
  Java native serialization, YAML `!!` tags). Forwarding third-party error
  bodies verbatim to clients. SQL/NoSQL/LDAP/XPath/command strings built by
  string-formatting third-party data.
- **Fix.** Treat third-party responses as **untrusted input**:
  - Validate against a strict response schema (JSON Schema / Pydantic / Zod).
  - **Parametrize** every SQL/NoSQL/LDAP/XPath query — never string-format.
  - **Never** `pickle.loads` / Java native deserialize data you didn't write.
  - Use `yaml.safe_load`, not `yaml.load`.
  - Enforce TLS, certificate verification, and timeouts on every outbound call.
  - Don't blindly forward error bodies — wrap them.

---

## Step 4: Cross-Cutting Checks

These slice across multiple OWASP items but warrant their own pass.

### JWT deep-dive (one more time, explicitly)

- ✅ Pin `algorithms=[…]` to exactly what you issue (RS256 or ES256 — not "any").
- ✅ Verify `iss`, `aud`, `exp`, `nbf`, `iat`; require their presence.
- ✅ Strong, rotated signing keys; HS256 only for server-to-server with a
  high-entropy secret stored in a secret manager.
- ✅ Short-lived access tokens + refresh-token rotation; revocation by `jti`.
- ✅ No PII in claims you wouldn't log. Scopes/roles checked per-route.
- 🚫 No `alg: none`. No HMAC verification with the RSA public key. No
  long-lived bearer tokens with no revocation path.

### CORS attack patterns

- 🚫 `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`
  (browsers reject — but reflection variants slip through anyway).
- 🚫 **Origin reflection** — the server echoes whatever `Origin` the client
  sends. A malicious site's origin gets reflected, and an authenticated
  cross-origin request is wide open.
- 🚫 **`null` origin** accepted (sandboxed iframes, file://, some redirects send
  `Origin: null`).
- 🚫 Pre-flight not enforced server-side — the server actually executes the
  unsafe request even though the browser would block the response read.
- ✅ Explicit **origin allowlist**; per-origin `Allow-Credentials`; reject
  `null`; validate the `Origin` header on every state-changing request.

### GraphQL specifics

- ✅ **Introspection disabled in production** (or restricted to authorized
  operators).
- ✅ Query **depth limit** (e.g. ≤ 10) and **complexity / cost analysis** limit.
- ✅ **Field-level authorization at every resolver** — in GraphQL, BOLA is
  per-field, not per-endpoint. A `User` type's `email` may be public; `phone`
  is not.
- ✅ Cap **aliases + batching** (a single HTTP request can contain many
  expensive operations).
- ✅ Watch **resolver-level N+1** — DataLoader or batch resolvers prevent shaped
  queries from melting the DB.
- ✅ **Persisted queries** (or APQ with an allowlist) for high-trust first-party
  clients; arbitrary queries only for partners that need them.

---

## Step 5: Red-Flags Quick Scan

Any one of these flips the review toward 🟠 or 🔴 — name it and find the others
nearby:

- `GET /users/{id}` returns **all** user fields (PII, `password_hash`, internal
  flags). → API3.
- `PATCH /users/{id}` accepts an **arbitrary body** with no schema (mass
  assignment). → API3.
- JWT verified with `verify=False` / `verify_signature=False`, or no
  `algorithms=` allowlist. → API2.
- CORS **origin reflection** (`Access-Control-Allow-Origin: <whatever the
  request sent>`). → API8.
- No `401` / `403` on `/admin/*` for a non-admin token. → API5.
- Rate limit only on `/login`, nowhere else (DoS, scraping, BOLA enumeration
  all uncapped). → API4.
- Endpoint accepts a **URL in the body** and fetches it server-side, with no
  allowlist. → API7.
- GraphQL **introspection enabled** on prod (`/graphql` returns `__schema`).
  → API9.
- Internal endpoints under `/v0/internal/*` reachable from the **public**
  domain. → API9.

---

## Step 6: Output Format — the Review Report

Lead with a verdict banner, then findings, then a coverage matrix, then
suggested test artefacts and the disclaimer.

### Verdict banner (pick one)

- ✅ **Hardened** — no Critical or High findings; baseline headers, auth, and
  rate limits in place.
- 🟡 **Multiple Medium issues — fix before prod scale-up.**
- 🟠 **High risk — patch the listed items before the next deploy.**
- ⛔ **Critical — block deploy.** One or more 🔴 findings (auth bypass, BOLA on
  PII, SSRF, mass assignment of privilege).

Follow with **one line** of *why* (e.g. *"BOLA on `/users/{userId}` and mass
assignment of `isAdmin` on `PATCH /users/{userId}`"*).

### Findings table

| # | OWASP ID | Endpoint / feature | Severity | One-liner |
|---|---|---|---|---|
| 1 | API1:2023 | `GET /users/{userId}` | 🔴 Critical | No owner check; any authenticated user reads any user. |
| 2 | API3:2023 | `PATCH /users/{userId}` | 🔴 Critical | Mass assignment — accepts `isAdmin`. |
| 3 | API8:2023 | global | 🟡 Medium | Missing HSTS / CSP / `X-Content-Type-Options`. |

### Per-finding detail

For each finding, write:

1. **Description** — what's wrong, in one short paragraph.
2. **Impact** — what an attacker gets.
3. **Curl test snippet** — a single command another engineer can run to
   reproduce:

   ```bash
   # API1:2023 — BOLA on /users/{userId}
   curl -i -H "Authorization: Bearer $ALICE_TOKEN" \
        https://api.example.com/users/$BOB_USER_ID
   # Expect: 403 / 404 (object hidden). Vulnerable: 200 + Bob's record.
   ```

4. **Code-level fix** — the exact snippet from Step 3 for that OWASP item,
   adapted to the user's stack.
5. **References** — link to the OWASP API Top 10 2023 page for that ID.

### Coverage matrix

So the caller knows what was actually checked vs not reviewed:

| OWASP ID | Name | Result |
|---|---|---|
| API1:2023 | Broken Object Level Authorization | ❌ fail (finding #1) |
| API2:2023 | Broken Authentication | ✅ pass |
| API3:2023 | Broken Object Property Level Authorization | ❌ fail (finding #2) |
| API4:2023 | Unrestricted Resource Consumption | 🟡 partial (rate limit OK, no body cap) |
| API5:2023 | Broken Function Level Authorization | ✅ pass |
| API6:2023 | Unrestricted Access to Sensitive Business Flows | ⚪ not reviewed (need flow inventory) |
| API7:2023 | Server-Side Request Forgery | ⚪ n/a (no outbound fetch in scope) |
| API8:2023 | Security Misconfiguration | ❌ fail (finding #3) |
| API9:2023 | Improper Inventory Management | ⚪ not reviewed (spec only) |
| API10:2023 | Unsafe Consumption of APIs | ⚪ n/a (no third-party calls in scope) |

### Suggested test artefacts

A small set the caller can drop into CI right now.

```python
# tests/test_bola.py — pytest regression for finding #1
def test_bola_users_endpoint(client, alice_token, bob_user_id):
    r = client.get(f"/users/{bob_user_id}", headers={"Authorization": f"Bearer {alice_token}"})
    assert r.status_code in (403, 404), f"BOLA: alice could read bob, got {r.status_code} {r.text}"
```

```yaml
# .github/workflows/api-security.yml
name: api-security
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 42Crunch API Security Audit
        uses: 42Crunch/api-security-audit-action@v4
        with:
          api-token: ${{ secrets.FORTYTWO_CRUNCH_TOKEN }}
          min-score: 75
      - name: OWASP ZAP baseline
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: https://staging.api.example.com
      - name: Schemathesis property-based tests
        run: |
          pip install schemathesis
          schemathesis run --checks all openapi.yaml \
            --base-url https://staging.api.example.com
```

### Disclaimer (always include)

> **Educational security guidance — not a substitute for a professional
> penetration test or a live DAST run, and not legal or compliance advice.**
> This review is **static reasoning over what you provided** (spec, source, or
> samples); it cannot hit a live server, observe runtime behaviour, or attest
> to PCI / HIPAA / SOC 2 conformance — those require qualified auditors. Fix
> the findings, then run a live scan (42Crunch, OWASP ZAP, Schemathesis,
> Burp) and, for high-value services, commission a third-party pen-test.

---

## Related Viprasol Skills

- **`code-review-skill`** — general correctness / security / performance review
  of the same PR with prioritized findings.
- **`smart-contract-audit`** — if the API fronts an on-chain protocol, audit
  the Solidity/EVM contracts behind it.

*Not affiliated with or endorsed by Anthropic.*

