From 3c8ebc6dc49a3f6369a4aef4d84bbf922693059e Mon Sep 17 00:00:00 2001 From: Angel Garcia Date: Sat, 6 Jun 2026 21:05:58 -0600 Subject: [PATCH] fix(skill): add GraphQL operation gates --- skills/appsec/api-security/SKILL.md | 53 ++++++++++++++++++- .../api-security/api-top10-checklist.md | 6 ++- .../benign/graphql-persisted-controls.md | 31 +++++++++++ .../vulnerable/graphql-alias-batch-bypass.md | 29 ++++++++++ ...raphql-default-cost-expensive-resolvers.md | 27 ++++++++++ .../graphql-persisted-query-bypass.md | 21 ++++++++ 6 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 skills/appsec/api-security/tests/benign/graphql-persisted-controls.md create mode 100644 skills/appsec/api-security/tests/vulnerable/graphql-alias-batch-bypass.md create mode 100644 skills/appsec/api-security/tests/vulnerable/graphql-default-cost-expensive-resolvers.md create mode 100644 skills/appsec/api-security/tests/vulnerable/graphql-persisted-query-bypass.md diff --git a/skills/appsec/api-security/SKILL.md b/skills/appsec/api-security/SKILL.md index cbb125aa..e6ec40d6 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 @@ -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. @@ -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 @@ -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] | ``` --- @@ -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 diff --git a/skills/appsec/api-security/api-top10-checklist.md b/skills/appsec/api-security/api-top10-checklist.md index b6569f61..cf688e68 100644 --- a/skills/appsec/api-security/api-top10-checklist.md +++ b/skills/appsec/api-security/api-top10-checklist.md @@ -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. diff --git a/skills/appsec/api-security/tests/benign/graphql-persisted-controls.md b/skills/appsec/api-security/tests/benign/graphql-persisted-controls.md new file mode 100644 index 00000000..8a40bcc7 --- /dev/null +++ b/skills/appsec/api-security/tests/benign/graphql-persisted-controls.md @@ -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. diff --git a/skills/appsec/api-security/tests/vulnerable/graphql-alias-batch-bypass.md b/skills/appsec/api-security/tests/vulnerable/graphql-alias-batch-bypass.md new file mode 100644 index 00000000..7074e3bb --- /dev/null +++ b/skills/appsec/api-security/tests/vulnerable/graphql-alias-batch-bypass.md @@ -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. diff --git a/skills/appsec/api-security/tests/vulnerable/graphql-default-cost-expensive-resolvers.md b/skills/appsec/api-security/tests/vulnerable/graphql-default-cost-expensive-resolvers.md new file mode 100644 index 00000000..b3c9fe42 --- /dev/null +++ b/skills/appsec/api-security/tests/vulnerable/graphql-default-cost-expensive-resolvers.md @@ -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. diff --git a/skills/appsec/api-security/tests/vulnerable/graphql-persisted-query-bypass.md b/skills/appsec/api-security/tests/vulnerable/graphql-persisted-query-bypass.md new file mode 100644 index 00000000..4f1adefd --- /dev/null +++ b/skills/appsec/api-security/tests/vulnerable/graphql-persisted-query-bypass.md @@ -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.