Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions skills/appsec/api-security/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,6 +38,7 @@ Before analyzing any endpoint, establish a complete inventory of the API surface
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.
8. **For GraphQL APIs, capture operation-control evidence** -- Depth limit, complexity budget, alias count, batch operation limit, persisted-query policy, introspection policy, subscription limits, resolver cost overrides, and federation/subgraph enforcement.

> **Gate:** Do not proceed until the API style, authentication model, authorization model, and endpoint inventory are documented. Incomplete scope leads to missed findings.

Expand Down Expand Up @@ -92,7 +93,7 @@ The final review output must be structured as follows:
**API Style:** [REST / GraphQL / gRPC / Hybrid]
**Specification:** [OpenAPI spec path, if applicable]
**Date:** [review date]
**Reviewer:** AI Agent -- api-security skill v1.0.0
**Reviewer:** AI Agent -- api-security skill v1.0.1

### Summary

Expand Down Expand Up @@ -129,6 +130,18 @@ The final review output must be structured as follows:
- **Status:** Open

[Repeat for each finding]

### GraphQL Operation Controls
| Control | Evidence | Status |
|---|---|---|
| Operation/batch limit | [max operations per request, batch behavior] | [Pass / Gap / Not Evaluable] |
| Alias limit and resolver throttling | [alias cap, duplicate resolver accounting, sensitive resolver limits] | [Pass / Gap / Not Evaluable] |
| Depth and complexity | [max depth, max complexity, timeout, rejection evidence] | [Pass / Gap / Not Evaluable] |
| Resolver cost overrides | [high-cost fields and configured weights] | [Pass / Gap / Not Evaluable] |
| Persisted-query/safelist enforcement | [unknown hash and raw document behavior] | [Pass / Gap / Not Evaluable] |
| Introspection/playground exposure | [environment and auth requirements] | [Pass / Gap / Not Evaluable] |
| Subscription/live query controls | [connection cap, idle timeout, event rate] | [Pass / Gap / Not Evaluable] |
| Federation/subgraph parity | [router/subgraph limit and auth propagation evidence] | [Pass / Gap / Not Evaluable] |
```

---
Expand Down Expand Up @@ -199,6 +212,42 @@ Unlike REST, where authorization can be enforced per endpoint, GraphQL requires

**Mitigation:** Count aliased operations against rate limits. Limit the number of aliases per request.

### GraphQL Operation-Control Evidence Gates

For GraphQL APIs, do not treat "depth limit enabled" or "introspection disabled" as sufficient evidence. Record the runtime controls that bound execution cost, sensitive resolver fan-out, and accepted document sources.

**Minimum evidence to collect:**

| Evidence Gate | Required Proof | Failure Mode |
|---|---|---|
| Operation and batch count | Maximum operations per HTTP request, JSON-array batch handling, named-operation selection behavior | One HTTP request can execute many sensitive operations |
| Alias and field fan-out | Alias limit, duplicate sensitive resolver accounting, per-resolver throttling for login/search/export mutations | Aliases bypass endpoint rate limits or brute-force controls |
| Depth and complexity | Configured max depth, max complexity, timeout, rejection status, and logging of rejected queries | Limits exist in code but are disabled, too high, or not enforced in production |
| Resolver cost overrides | Cost matrix for database fan-out, search, export, third-party API, and nested connection fields | Expensive resolvers keep default cost and understate actual resource use |
| Persisted query / safelist enforcement | Production policy, unknown-hash rejection, raw document rejection, rollout exceptions | Production accepts arbitrary raw GraphQL documents despite persisted-only policy |
| Introspection and playground exposure | Environment-specific introspection/playground policy and authentication requirements | Introspection disabled but playground/raw query endpoint remains exposed |
| Subscription and live query controls | Connection limits, per-user subscription caps, idle timeout, max event rate, backpressure behavior | Long-lived operations bypass per-request accounting |
| Federation / subgraph parity | Router and subgraph limits, auth propagation, cost/depth enforcement at both layers | Supergraph enforces limits but subgraphs can be queried or overloaded directly |

**What to look for:**

```
GQL-OPS-01: GraphQL batching or multiple operations per request bypasses rate limits
GQL-OPS-02: Alias fan-out is not counted against sensitive resolver throttles
GQL-OPS-03: Depth or complexity limits are missing, disabled, or not evidenced in production
GQL-OPS-04: Expensive resolvers use default cost weights
GQL-OPS-05: Persisted-query/safelist policy can be bypassed with raw query documents
GQL-OPS-06: Introspection is disabled but playground/raw query access remains exposed
GQL-OPS-07: Subscriptions or live queries lack connection, duration, and event-rate limits
GQL-OPS-08: Federation router and subgraph limits are inconsistent or subgraphs are directly reachable
```

**False-positive guardrails:**

- Do not flag a bounded GraphQL endpoint solely because it supports aliases or variables; require evidence that aliases, operations, and resolver cost are unbounded or not enforced.
- Do not require persisted queries for every internal-only GraphQL API if raw documents are authenticated, rate-limited, complexity-scored, logged, and not reachable from untrusted clients.
- Do not downgrade resolver authorization based only on parent-object checks; verify whether nested fields, fragments, aliases, DataLoader batches, and federation entity resolvers reuse the same policy.

---

## Common Pitfalls
Expand Down
6 changes: 5 additions & 1 deletion skills/appsec/api-security/api-top10-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,11 @@ 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.
- [ ] Request body size limits are configured.
- [ ] GraphQL queries have depth limits, complexity limits, and batch restrictions.
- [ ] GraphQL queries have depth limits, complexity limits, batch restrictions, alias limits, and operation-count limits.
- [ ] GraphQL persisted-query or safelist policy rejects unknown hashes and raw query documents when enabled.
- [ ] GraphQL resolver cost weights are calibrated for database fan-out, search, export, and third-party API calls.
- [ ] GraphQL subscriptions or live queries have connection, duration, and event-rate controls.
- [ ] GraphQL federation routers and subgraphs enforce equivalent auth, depth, complexity, and rate limits.
- [ ] Database queries and downstream calls have execution timeouts.
- [ ] Billable operations have cost controls and alerting.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Benign: bounded persisted GraphQL controls

## Scenario

Production GraphQL endpoint accepts only persisted query hashes from untrusted clients.

```yaml
graphql_controls:
persisted_queries_required: true
unknown_hash_behavior: reject_400
raw_query_documents_from_public_clients: reject_400
max_depth: 6
max_complexity: 500
max_operations_per_request: 1
json_array_batching: disabled
alias_limit: 10
introspection: disabled_in_prod
playground: disabled_in_prod
resolver_cost_overrides:
Order.lineItems: 20
Search.results: 50
Report.exportUrl: 100
subscription_limits:
max_connections_per_user: 3
idle_timeout_seconds: 300
max_events_per_minute: 120
```

## Expected Result

Do not raise `GQL-OPS-*` findings. The endpoint has evidenced persisted-query enforcement, bounded execution cost, alias/operation limits, subscription limits, and resolver cost overrides.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Vulnerable: alias and batch fan-out bypass sensitive resolver controls

## Scenario

The gateway rate limit is one request per second, but the GraphQL executor allows many operations and aliases in one HTTP request.

```graphql
query PasswordGuesses {
a1: login(email: "user@example.com", password: "guess1") { token }
a2: login(email: "user@example.com", password: "guess2") { token }
a3: login(email: "user@example.com", password: "guess3") { token }
a4: login(email: "user@example.com", password: "guess4") { token }
}
```

```yaml
graphql_controls:
gateway_rate_limit: "1 request/second"
max_operations_per_request: 10
alias_limit: unlimited
sensitive_resolver_throttle:
login: not_configured
duplicate_resolver_accounting: false
```

## Expected Findings

- `GQL-OPS-01` because multiple operations per request can bypass request-level throttles.
- `GQL-OPS-02` because alias fan-out is not counted against sensitive resolver throttles.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Vulnerable: expensive resolvers keep default complexity cost

## Scenario

The API has a complexity plugin, but high-cost fields keep the default field cost.

```yaml
complexity_plugin:
enabled: true
max_complexity: 1000
resolver_costs:
User.orders: 1
Search.results: 1
Report.exportUrl: 1
Billing.invoicePdf: 1
runtime_profile:
Search.results:
database_queries_per_call: 8
third_party_calls_per_call: 1
Report.exportUrl:
starts_async_export_job: true
```

## Expected Findings

- `GQL-OPS-04` because database fan-out, export, and third-party-call resolvers use default cost weights.
- `GQL-OPS-03` if complexity rejection evidence is not logged or tested.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Vulnerable: persisted-query policy accepts raw documents

## Scenario

Production policy says public clients must use persisted query hashes, but raw query documents are still accepted when a hash is unknown.

```yaml
prod_policy:
persisted_queries_required: true
introspection: disabled
observed_behavior:
unknown_sha256_hash: fallback_to_raw_query
raw_query_document: accepted
rejection_status_for_unknown_hash: none
complexity_rejection_log: missing
```

## Expected Findings

- `GQL-OPS-05` because persisted-query/safelist policy can be bypassed with raw query documents.
- `GQL-OPS-03` if no depth/complexity rejection evidence is available for raw documents.