diff --git a/skills/appsec/api-security/SKILL.md b/skills/appsec/api-security/SKILL.md index a5d56b53..eee7443c 100644 --- a/skills/appsec/api-security/SKILL.md +++ b/skills/appsec/api-security/SKILL.md @@ -34,7 +34,7 @@ Before analyzing any endpoint, establish a complete inventory of the API surface 1. **Identify the API style** -- REST (OpenAPI/Swagger), GraphQL, gRPC, or hybrid. Each style has distinct attack patterns. 2. **Catalog all endpoints and operations** -- For REST, list every path and HTTP method. For GraphQL, list all queries, mutations, and subscriptions. 3. **Map authentication mechanisms** -- OAuth 2.0 flows, API keys, JWTs, session cookies, mTLS, or custom tokens. Note which endpoints require authentication and which are public. -4. **Identify authorization models** -- RBAC, ABAC, ownership-based, or no authorization. Document how object-level and function-level access control decisions are made. +4. **Identify authorization models** -- RBAC, ABAC, ownership-based, or no authorization. Document how object-level and function-level access control decisions are made, including how cursor-based pagination and filter changes are re-checked against tenant/user scope. 5. **Catalog data objects** -- List the resources/entities exposed by the API and their sensitivity classification (PII, financial, internal, public). 6. **Note rate limiting and quota configurations** -- Document any existing throttling, quota, or cost-control mechanisms at the gateway or application layer. 7. **Identify downstream dependencies** -- Third-party APIs, internal microservices, or webhooks that the API consumes. @@ -217,6 +217,8 @@ Unlike REST, where authorization can be enforced per endpoint, GraphQL requires 6. **Ignoring upstream API trust.** Data received from third-party APIs and even internal microservices must be validated before use. A compromised upstream service can inject SQL, XSS, or SSRF payloads through otherwise trusted data channels. +7. **Flagging cursor pagination without proof of scope breakage.** Opaque cursors are not automatically vulnerable. Treat them as a finding only when evidence shows cross-tenant replay, stale permission reuse, or missing authorization checks on the follow-up page request. + --- ## Limitations diff --git a/skills/appsec/api-security/api-top10-checklist.md b/skills/appsec/api-security/api-top10-checklist.md index b6569f61..01f4460e 100644 --- a/skills/appsec/api-security/api-top10-checklist.md +++ b/skills/appsec/api-security/api-top10-checklist.md @@ -17,6 +17,8 @@ BOLA occurs when an API endpoint accepts an object identifier from the client an - Authorization logic that checks only whether the user is authenticated, not whether they own or have access to the specific object. - Sequential or predictable resource identifiers (auto-increment integers) that enable enumeration. - Batch or list endpoints that return objects without filtering by the caller's permissions. +- Cursor-based pagination or search tokens that can be replayed across tenant, account, or filter boundaries. +- Opaque cursors that encode a sort key or record ID but omit tenant scope, permission version, or server-side revalidation on each page request. ### REST Vulnerable Patterns @@ -80,6 +82,40 @@ const resolvers = { }; ``` +### Cursor Pagination Patterns + +```python +# VULNERABLE: Cursor advances by ID only; tenant scope is never re-checked +@app.route('/api/v1/invoices') +@require_auth +def list_invoices(): + cursor = request.args.get("cursor") + query = Invoice.query.order_by(Invoice.id.asc()) + if cursor: + query = query.filter(Invoice.id > decode_cursor(cursor)["last_id"]) + invoices = query.limit(50).all() + return jsonify([invoice.to_dict() for invoice in invoices]) +``` + +```python +# SECURE: Cursor stays bound to tenant scope and current authorization +@app.route('/api/v1/invoices') +@require_auth +def list_invoices(): + cursor = request.args.get("cursor") + scope = {"tenant_id": current_user.tenant_id, "role": current_user.role} + query = Invoice.query.filter_by(tenant_id=current_user.tenant_id).order_by(Invoice.id.asc()) + if cursor: + decoded = decode_cursor(cursor) + if decoded["tenant_id"] != current_user.tenant_id: + return jsonify({"error": "Invalid cursor"}), 400 + query = query.filter(Invoice.id > decoded["last_id"]) + invoices = query.limit(50).all() + return jsonify([invoice.to_dict() for invoice in invoices]) +``` + +**Benign exception:** Do not flag cursor pagination by default when the cursor is opaque, the server binds it to tenant/user scope, and every page request re-checks authorization before returning results. The finding needs proof that a cursor from one scope can fetch data from another scope or outlive a permission change. + ### BOLA vs BFLA Distinction BOLA and BFLA (API5:2023) are frequently confused. The distinction is critical for accurate findings: @@ -99,8 +135,11 @@ Both can coexist in a single endpoint. An endpoint may lack both a role check (B - [ ] Every endpoint that accepts a resource identifier enforces ownership or relationship-based access control. - [ ] Authorization checks happen at the data access layer, not only at the controller/route layer. - [ ] Batch/list endpoints filter results by the caller's permissions. +- [ ] Cursor-based pagination and search tokens are bound to tenant/user scope and revalidated on every follow-up request. - [ ] Resource identifiers are UUIDs or non-sequential values to resist enumeration. - [ ] GraphQL resolvers enforce authorization on every field that returns sensitive data. +- [ ] Findings about cursor tenant isolation include concrete evidence that cross-tenant replay, stale authorization reuse, or filter-bypass is actually possible. +- [ ] Remediation includes a regression test proving a cursor from tenant A cannot fetch tenant B data after filter, role, or membership changes. ---