diff --git a/skills/appsec/api-security/SKILL.md b/skills/appsec/api-security/SKILL.md index cbb125aa..74753ddc 100644 --- a/skills/appsec/api-security/SKILL.md +++ b/skills/appsec/api-security/SKILL.md @@ -11,7 +11,7 @@ phase: [design, build, review] frameworks: [OWASP-API-Security-2023, OWASP-ASVS] difficulty: intermediate time_estimate: "20-40min" -version: "1.0.0" +version: "1.0.1" author: unitoneai license: MIT allowed-tools: Read, Grep, Glob @@ -36,8 +36,9 @@ Before analyzing any endpoint, establish a complete inventory of the API surface 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. 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. +6. **Catalog list and pagination patterns** -- Offset/limit, cursor, Relay-style GraphQL connections, export jobs, and search endpoints. Record whether cursors carry authorization, filter, sort, or snapshot state. +7. **Note rate limiting and quota configurations** -- Document any existing throttling, quota, or cost-control mechanisms at the gateway or application layer. +8. **Identify downstream dependencies** -- Third-party APIs, internal microservices, or webhooks that the API consumes. > **Gate:** Do not proceed until the API style, authentication model, authorization model, and endpoint inventory are documented. Incomplete scope leads to missed findings. @@ -112,6 +113,12 @@ The final review output must be structured as follows: **Total Findings:** [count] **Critical:** [count] | **High:** [count] | **Medium:** [count] | **Low:** [count] | **Info:** [count] +### Cursor and Pagination Evidence + +| Endpoint / Operation | Pagination Pattern | Page Size Enforcement | Cursor Integrity | Principal/Tenant Binding | Sort/Snapshot Evidence | Replay/Expiry | Status | +|---|---|---|---|---|---|---|---| +| [path/query] | [offset/limit/cursor/Relay/export] | [server-fixed/capped/missing] | [signed/opaque/client-controlled/N/A] | [verified/missing/N/A] | [stable tuple/snapshot/high-watermark/unknown] | [bounded/unbounded/N/A] | [Pass/Finding/Not Evaluable] | + ### Findings #### API-SEC-001: [Title] @@ -215,6 +222,16 @@ 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. **Treating cursor pagination as only a page-size issue.** Cursors can carry authorization state, tenant scope, filter scope, sort position, or snapshot state. A capped page size is not sufficient if the cursor is client-controlled, reusable across endpoints, missing expiry, or not revalidated against the current principal. + +8. **Base64 encoding a cursor and calling it opaque.** Encoding is not integrity protection. Cursor values that affect tenant, user, filter, sort, or scan position must be server-issued, tamper-resistant, endpoint/audience-bound, and safe when replayed. + +--- + +## Changelog + +- **v1.0.1** -- Added cursor pagination evidence gates for tenant/principal binding, cursor integrity, endpoint audience, server-side page size, stable ordering, snapshot/high-watermark behavior, replay, and expiry. + --- ## Prompt Injection Safety Notice diff --git a/skills/appsec/api-security/api-top10-checklist.md b/skills/appsec/api-security/api-top10-checklist.md index b6569f61..6f14646f 100644 --- a/skills/appsec/api-security/api-top10-checklist.md +++ b/skills/appsec/api-security/api-top10-checklist.md @@ -102,6 +102,76 @@ Both can coexist in a single endpoint. An endpoint may lack both a role check (B - [ ] Resource identifiers are UUIDs or non-sequential values to resist enumeration. - [ ] GraphQL resolvers enforce authorization on every field that returns sensitive data. +### Cursor Pagination Authorization Gate + +Cursor pagination can be a BOLA vector when the cursor embeds tenant, user, filter, sort, or object-position state that is trusted without integrity protection and current authorization checks. + +```python +# VULNERABLE: Client-controlled cursor changes tenant scope +@app.route('/api/v1/accounts') +@require_auth +def list_accounts(): + cursor = json.loads(base64.urlsafe_b64decode(request.args['cursor'])) + tenant_id = cursor.get('tenant_id', current_user.tenant_id) + after_id = cursor.get('after_id') + + rows = ( + Account.query + .filter(Account.tenant_id == tenant_id) + .filter(Account.id > after_id) + .limit(100) + .all() + ) + return jsonify([row.to_dict() for row in rows]) +``` + +Remediation: + +```python +# SECURE: Server-issued cursor is integrity-protected and revalidated +@app.route('/api/v1/accounts') +@require_auth +def list_accounts(): + claims = verify_cursor( + request.args.get('cursor'), + audience='accounts:list', + max_age_seconds=900, + ) + if claims and claims['tenant_id'] != current_user.tenant_id: + abort(403) + + page_size = 50 + rows = ( + Account.query + .filter(Account.tenant_id == current_user.tenant_id) + .filter( + tuple_(Account.created_at, Account.id) < + (claims['last_seen_at'], claims['last_id']) + if claims else True + ) + .order_by(Account.created_at.desc(), Account.id.desc()) + .limit(page_size + 1) + .all() + ) + return jsonify({ + 'items': [row.to_dict() for row in rows[:page_size]], + 'next_cursor': sign_cursor({ + 'tenant_id': current_user.tenant_id, + 'aud': 'accounts:list', + 'last_seen_at': rows[-1].created_at.isoformat(), + 'last_id': rows[-1].id, + }) if len(rows) > page_size else None, + }) +``` + +Review checklist: + +- [ ] Cursor values are server-issued and integrity-protected; base64 encoding alone is not accepted. +- [ ] Cursor claims are bound to the authenticated principal, tenant, endpoint/audience, query shape, and page-size policy. +- [ ] Authorization is rechecked on every cursor request, not only when the first page is issued. +- [ ] GraphQL Relay-style cursors follow the same integrity, tenant-binding, and authorization rules. +- [ ] Cursor reuse across endpoints or lower-sensitivity lists is rejected. + --- ## API2:2023 -- Broken Authentication @@ -267,10 +337,35 @@ query { app.use(express.json()); // Default limit may be very large or unconfigured ``` +```javascript +// VULNERABLE: Cursor page size is capped, but ordering is unstable and cursor is replayable +app.get('/api/v1/audit-events', requireAuth, async (req, res) => { + const cursor = JSON.parse(Buffer.from(req.query.cursor || '{}', 'base64url')); + const rows = await db.auditEvents.findMany({ + where: { + tenantId: req.user.tenantId, + createdAt: { lt: cursor.createdAt || new Date() } + }, + orderBy: { createdAt: 'desc' }, + take: 100 + }); + + res.json({ + items: rows, + next_cursor: Buffer.from(JSON.stringify({ + createdAt: rows.at(-1)?.createdAt + })).toString('base64url') + }); +}); +``` + ### Remediation Guidance - Implement rate limiting at the API gateway and/or application layer. Use sliding window or token bucket algorithms. Set per-endpoint limits based on expected legitimate usage. - Enforce maximum pagination size (e.g., `limit` capped at 100). Default to a reasonable page size (e.g., 20). +- For cursor pagination, enforce a server-side page size, sign or otherwise integrity-protect the cursor, bind it to one endpoint/query shape and principal/tenant, and set a bounded lifetime. +- Use a deterministic unique sort tuple (for example, `(created_at, id)` rather than `created_at` alone) and document whether the list is snapshot-consistent, high-watermark based, or intentionally eventually consistent. +- Define safe behavior for stale or replayed cursors: reject, restart from a bounded high-watermark, or return a consistent error. Monitor repeated cursor scans against expensive endpoints. - Set maximum request body sizes (`express.json({ limit: '1mb' })`). - For GraphQL: enforce query depth limits (e.g., max depth 5), complexity analysis (weighted field costs), and batch query limits. - Set execution timeouts for database queries and downstream API calls. @@ -280,6 +375,8 @@ app.use(express.json()); // Default limit may be very large or unconfigured - [ ] Rate limiting is configured for all endpoints, with stricter limits on expensive operations. - [ ] Pagination has a maximum page size enforced server-side. +- [ ] Cursor pagination uses a stable unique sort tuple and documents snapshot, high-watermark, or eventual-consistency behavior. +- [ ] Cursor replay, expiry, audience binding, and repeated-scan monitoring are defined for expensive list/search/export endpoints. - [ ] Request body size limits are configured. - [ ] GraphQL queries have depth limits, complexity limits, and batch restrictions. - [ ] Database queries and downstream calls have execution timeouts.