GraphQL is not less secure than REST, but it moves the attack surface to places that REST-era controls do not look. A REST API spreads risk across many routes, where the classic failures are endpoint sprawl, forgotten versions and broken object level authorisation on one route out of hundreds. A GraphQL API concentrates everything behind one endpoint, where the failures are unbounded queries, batching that defeats rate limits and authorisation that holds on one resolver path but not another. This guide compares the two styles control by control: threat model, where authorisation must sit, why gateways and web application firewalls see less in GraphQL, and which controls each style needs. It is for API architects, AppSec engineers and API security buyers. For GraphQL implementation depth, read the GraphQL Security Guide; for REST-wide basics, the API Security Guide.
Key takeaways
- Same top risks, different shape. Broken object level authorisation (OWASP API1:2023) and unrestricted resource consumption (API4:2023) hit both styles, but in REST they hide in route sprawl and in GraphQL they hide in resolver paths and query shape.
- Authorisation belongs below both. graphql.org recommends enforcing authorisation in the business logic layer as a single source of truth; the same layer should serve REST routes, so the two APIs cannot disagree.
- Request counting breaks in GraphQL. OWASP notes that HTTP-level rate limits count requests, not query cost or batched operations; GraphQL needs depth, breadth, batch and cost limits inside the server.
- Edge tools see less. A gateway or WAF routes and filters by method and path; in GraphQL the operation, fields and arguments live in the request body behind one URL.
- Persisted operations change the game. For first-party GraphQL clients, an allowlist of trusted documents is, in graphql.org's words, likely the most effective single control.
Short answer: the two styles side by side
| Dimension | REST | GraphQL |
|---|---|---|
| Entry points | Many routes and methods, often across versions | Usually one endpoint accepting arbitrary operations against the schema |
| Unit of work | Roughly one request, one resource action | One request can be many operations, aliases and nested lists |
| Where authorisation is commonly enforced | Route or controller, per method | Resolver per field, edge and node |
| Typical discovery risk | Shadow and zombie endpoints, undocumented versions (API9:2023) | Introspection and field suggestions revealing the schema |
| Resource exhaustion vector | Large page sizes, expensive endpoints, request floods | Deep nesting, wide aliasing, batched operations |
| Rate limiting basis | Requests per client per route usually works | Must account for query cost and operations per request |
| Visibility for gateways and WAFs | Method and path identify the action | Action is in the body; path is the same for every call |
| Over-exposure of data | Endpoints returning full objects (API3:2023) | Clients choose fields; sensitive fields need field-level rules |
| Strongest structural control | Complete inventory plus schema-validated routes (OpenAPI) | Trusted documents plus demand control |
REST attack surface: threat model and failure modes
Threat model: an attacker enumerates routes, manipulates object identifiers in paths, query strings, headers or bodies, and looks for older or forgotten versions with weaker controls. OWASP API1:2023 describes broken object level authorisation as attackers changing an object ID to reach objects that belong to other users, and calls for object-level checks on every endpoint that takes an object ID. OWASP API9:2023 describes old versions, deprecated endpoints and unneeded hosts left exposed, often found by simple DNS enumeration.
Architecture in words: clients call versioned routes such as /v2/accounts/{id}/statements through an API gateway that authenticates the token, applies coarse rate limits and forwards to services. Each controller or handler is responsible for checking that the caller may perform the method on the specific resource.
- Endpoint sprawl: hundreds of routes across microservices mean hundreds of places to forget an object check. One missed route is a BOLA. See BOLA vs BFLA.
- Zombie versions:
/v1stays live after/v2adds a fix; API9 advises applying external protections to all exposed versions and retiring or backporting. - Method confusion: a route allows
DELETEorPUTit should not. The OWASP REST Security Cheat Sheet says to allow only approved methods, reject others with 405 and check that the caller may use that method on that resource. - Secrets in URLs: tokens or API keys in query strings end up in logs; the cheat sheet says to place them in headers or the body.
GraphQL attack surface: threat model and failure modes
Threat model: an attacker learns the schema through introspection or by guessing from "did you mean" suggestions, then crafts operations that are expensive (deep or wide), batched (many attempts in one request) or that reach a sensitive object through a path the developer did not protect. OWASP's GraphQL Cheat Sheet notes that depth and amount limits "can both be unlimited" by default, which may lead to denial of service.
Architecture in words: clients send operations to a single endpoint such as /graphql; the server parses and validates the document against the schema, then executes resolvers per field. A given object, such as an order, can be reached from a top-level query, through a customer's orders edge, from a search result or by a global node lookup, and each is a separate resolver.
- Introspection and suggestions: graphql.org notes that disabling introspection is security through obscurity: an attacker can infer the schema from validation errors. Turn off suggestions and mask errors outside development as well.
- Depth and breadth: nested lists grow results exponentially, and aliasing one field many times multiplies backend calls even in a shallow query. graphql.org recommends a depth limit, a smaller limit for nested lists, and caps on top-level fields and aliases.
- Batching for brute force: many login or one-time code attempts packed into one HTTP request evade per-request monitoring (ATT&CK T1110 Brute Force). OWASP recommends blocking batching for sensitive objects such as passwords, OTPs and session tokens, and capping operations per batch.
- Path-dependent authorisation: a check on the top-level query does not protect the same object reached through an edge; OWASP says to enforce authorisation "on both edges and nodes".
- Cross-site request forgery: the GraphQL over HTTP working draft warns that non-JSON media types such as
multipart/form-datacan produce simple cross-origin requests that skip browser preflight, and requires that GET never executes mutations.
Where authorisation must sit: route versus resolver versus business layer
REST teams often place authorisation in middleware keyed on route and method. That model maps poorly to GraphQL, where one endpoint serves every action. Putting checks in each resolver works, but graphql.org warns that duplicating logic per entry point means that "if the authorization logic is not kept perfectly in sync, users could see different data depending on which API they use". The durable answer for both styles is the same: enforce authorisation in a business logic or domain layer that REST controllers and GraphQL resolvers both call.
| Placement | REST | GraphQL | Risk if used alone |
|---|---|---|---|
| Gateway or edge | Good for authentication, coarse scope checks per route | Authentication only; cannot see fields or objects without parsing | No object-level decisions; BOLA passes straight through |
| Route middleware or controller | Common and workable for function-level checks | Not applicable: one route | Object checks forgotten on new routes |
| Resolver | Not applicable | Per field, edge and node | Inconsistent across paths to the same object |
| Schema directives | Not applicable | Declarative markers such as @auth on fields | graphql.org says logic should still be delegated to the business layer |
| Business logic layer or object loader | Called by every controller | Called by every resolver and node loader | Lowest: one source of truth for both APIs |
- Pass a resolved caller, not a token: graphql.org recommends a fully hydrated user object in context, keeping authentication and authorisation in separate stages. The same applies to REST handlers.
- Fail closed at the object loader: authorise when an object is loaded by ID, so every path (REST route, GraphQL query, edge,
node) passes the same check. - Batch policy decisions in GraphQL: one query can touch thousands of objects; calling an external policy engine per object will not scale. See the OpenID AuthZEN Authorization API Guide for standardised decision calls.
- Bind input fields explicitly: REST mass assignment and GraphQL mutation inputs share the same fix: allow only listed fields (OWASP API3:2023, broken object property level authorisation).
Why gateways and WAFs see less in GraphQL
A REST call carries its intent in the method and path: DELETE /v2/users/123 can be routed, logged, rate limited and blocked at the edge. A GraphQL call is usually POST /graphql with the operation in a JSON body. The edge sees the same path for a harmless profile read and a bulk export. The GraphQL Security Guide puts it simply: gateways and WAFs see one GraphQL endpoint, while the operation, fields and arguments live in the body.
- Signature rules lose context: injection payloads sit inside GraphQL variables, so generic rules either miss them or produce false positives on legitimate query text.
- Rate limits count the wrong thing: OWASP notes HTTP-level rate limits count requests, not query cost or batched operations. graphql.org recommends rate limiting in the business logic layer because the network layer cannot see field cost.
- Logs lose meaning: access logs show one URL. Log the operation name, a document hash, cost and the caller in the GraphQL server.
- What the edge still does well: TLS, authentication, IP reputation, request size limits, blocking GET mutations and enforcing
Content-Type: application/json. Some gateways can parse GraphQL; treat that as an extra layer, not a replacement for server-side limits. See WAF vs API Gateway and the API Gateway Security Guide.
Controls per style
| Risk | REST control | GraphQL control |
|---|---|---|
| Discovery of attack surface | Complete inventory of hosts, versions and environments; restrict documentation to authorised users (API9) | Disable introspection and GraphiQL in production; disable field suggestions; mask errors |
| Unknown or unexpected requests | Validate requests against an OpenAPI schema; reject unknown routes and methods (405) | Trusted documents (persisted operations): clients send an ID, the server runs only registered documents |
| Resource exhaustion (API4) | Pagination limits, request size limits (413), 429 rate limiting per client | Depth, list depth, breadth and alias limits; operations per batch; query cost analysis with per-request maximum or client budget; resolver timeouts |
| Object authorisation (API1) | Object check in every handler, via a shared domain layer | Object check in the object loader covering every edge and node; remove node/nodes if unused |
| Property exposure (API3) | Response schemas that return only needed fields | Field-level authorisation; interfaces and unions to return different shapes by role |
| Brute force via the API | Per-route rate limits on authentication endpoints | Block batching for credential and OTP operations; per-object attempt limits in code |
| CSRF in browser clients | Standard CSRF defences; JSON only for state changes | No mutations over GET (405); require application/json; consider a custom header that forces preflight |
| Input validation (SI-10) | Type, length, range, format checks; 415 for unsupported content types | Schema types plus custom scalars and argument validation; parameterised data access in resolvers |
Persisted operations caveat: graphql.org notes trusted documents work best for first-party APIs, since a public API cannot know third-party operations in advance. Public GraphQL APIs rely on cost analysis, per-client budgets and strong authorisation instead. graphql.org also notes efforts to standardise persisted documents in the GraphQL over HTTP specification, which is a working draft as of November 2026, so check the current status before depending on specific wire details.
Decision guidance: choosing and securing a style
- Public partner or third-party API with stable resources: REST is simpler to rate limit, cache and filter at the edge. Invest in inventory and per-handler object checks.
- First-party web and mobile clients with complex data needs: GraphQL with trusted documents gives you an allowlist that REST rarely achieves. Without persisted operations, budget for cost analysis and GraphQL-aware testing.
- Public GraphQL API: possible, but treat cost limits, per-client budgets, error masking and field-level authorisation as launch blockers, not hardening.
- Running both: common when GraphQL sits in front of REST microservices. Enforce authorisation in the services or domain layer, not only in the GraphQL layer, so a direct call to the REST service cannot bypass it. Federated graphs add the same question at every subgraph: each one must enforce its own object checks.
- Small teams: REST with an OpenAPI contract and a gateway is easier to secure with fewer specialists. GraphQL demands someone who understands resolver execution and demand control.
Worked scenario: one data model, two APIs
A retailer exposes orders through a REST API for partners and a GraphQL API for its own app.
- REST finding: a penetration test finds
GET /v1/orders/{id}still live after the/v2rewrite;/v1has no ownership check. Fix: retire/v1, add the inventory record, and move the ownership check into the order service so both versions share it. - GraphQL finding:
order(id)checks ownership, butsearch { results { ... on Order } }andnode(id)return any order. Fix: authorise in the order loader that every resolver uses, and removenodebecause the app does not need it. - Abuse finding: the app's login mutation accepts 100 aliased attempts in one request; the gateway rate limit of 10 requests per minute never trips. Fix: block batching and aliasing for authentication mutations and count attempts per account in code.
- Prevention: the app's build registers its operations as trusted documents; production rejects anything else. Partner REST traffic is validated against the OpenAPI contract at the gateway.
- Result: one authorisation layer, two thin API layers, and edge controls used for what they see well.
Testing, detection and metrics
Test both styles with authorisation-focused test cases: swap object IDs between two test users on every route and every GraphQL path to the same object. OWASP API1 recommends writing tests that evaluate the authorisation mechanism and blocking deployments that fail them. For GraphQL, add tests that send over-depth, over-breadth and over-batch operations and confirm rejection before execution. See the API Security Checklist and Rate Limiting and API Abuse Guide.
- Inventory coverage: percentage of live REST hosts and versions, and GraphQL endpoints, recorded in the API inventory with an owner.
- Authorisation test coverage: percentage of routes and GraphQL object types with automated cross-tenant or cross-user tests in CI.
- Rejected operations: count of GraphQL operations rejected for depth, cost, batch size or unknown document, per client, trended over time.
- Introspection and suggestion exposure: number of production GraphQL endpoints answering introspection (target zero for private APIs).
- Zombie version count: deprecated REST versions still receiving traffic past their retirement date.
- Cost budget breaches: clients exceeding their GraphQL cost budget, investigated as potential abuse.
Where identity fits
Most API callers are not people: mobile app instances, partner integrations, internal services and, increasingly, AI agents, each holding an API key or OAuth token. Scope tokens to the operations they need, bind GraphQL trusted documents and cost budgets to the client identity, and pass the originating user alongside a service identity when a service calls on someone's behalf. See the OAuth 2.0 and OpenID Connect Guide.
Framework mapping
| Framework | Reference | REST and GraphQL relevance |
|---|---|---|
| OWASP API Security Top 10 2023 | API1 Broken Object Level Authorization; API3 Broken Object Property Level Authorization; API4 Unrestricted Resource Consumption; API5 Broken Function Level Authorization; API9 Improper Inventory Management | Common risk set for both styles |
| NIST SP 800-53 Rev 5 | AC-3 Access Enforcement; AC-6 Least Privilege | Object, field and function authorisation |
| NIST SP 800-53 Rev 5 | SC-5 Denial-of-service Protection; SI-10 Information Input Validation; CM-8 System Component Inventory | Demand control, input validation, API inventory |
| NIST CSF 2.0 | PR.AA-05; ID.AM-02 | Authorisation policy and inventories of services |
| MITRE ATT&CK v20 | T1190 Exploit Public-Facing Application; T1110 Brute Force; T1595 Active Scanning | API exploitation, batched credential attacks, endpoint and schema discovery |
Common failure patterns
- Porting REST middleware to GraphQL: a route-based check on
/graphqlauthorises everything or nothing. - Relying on the gateway rate limit for GraphQL: one request can carry hundreds of operations or a query that costs thousands of backend calls.
- Disabling introspection and calling it done: suggestions and errors still leak the schema, and authorisation gaps remain.
- Authorising the top-level field only: the same object leaks through an edge, a search result or
node. - Forgetting REST versions: the new version is hardened; the old one still answers and shares the same database.
- GraphQL over REST with checks only in GraphQL: the backing REST services trust any internal caller, so a compromised service or token bypasses every rule.
Practitioner checklist
- Move object and field authorisation into a shared domain layer or object loader used by both REST handlers and GraphQL resolvers.
- Keep a complete inventory of REST hosts and versions and of GraphQL endpoints, with owners and retirement dates.
- Disable introspection, GraphiQL and field suggestions in production for private GraphQL APIs, and mask errors.
- Set depth, list depth, breadth, alias and batch limits, and add cost analysis where clients can write arbitrary queries.
- Use trusted documents for first-party GraphQL clients.
- Block batching for credential, OTP and session operations.
- Reject GET mutations and require
application/jsonfor GraphQL; restrict REST methods per route with 405. - Add cross-user authorisation tests for every route and every GraphQL path to sensitive objects, and fail the build on regressions.
- Log GraphQL operation name, document hash, cost and caller identity, not just the URL.
Standards and references
- OWASP: API1:2023 Broken Object Level Authorization
- OWASP: API9:2023 Improper Inventory Management
- OWASP: GraphQL Cheat Sheet
- OWASP: REST Security Cheat Sheet
- GraphQL Foundation: Security
- GraphQL Foundation: Authorization
- GraphQL Foundation: GraphQL over HTTP (working draft)
- NIST: SP 800-53 Rev 5, Security and Privacy Controls
Related NHI Mgmt Group resources: GraphQL Security Guide, API Security Guide, BOLA vs BFLA, WAF vs API Gateway, Rate Limiting and API Abuse Guide.