Why GraphQL needs different security thinking
REST APIs have a fixed attack surface — each endpoint returns a predictable response. GraphQL exposes a single endpoint with a flexible query language, which means traditional API security patterns don't translate directly. Rate limiting by endpoint doesn't work when every request goes to /graphql. Input validation changes when the client controls the query structure. Authorization gets complex when nested resolvers return data from different access levels.
Nigerian fintechs increasingly adopt GraphQL for mobile banking apps and internal dashboards. The flexibility is appealing, but the security surface is wider than most teams realise.
1. Query depth limiting
GraphQL schemas are often deeply nested. A user has transactions, transactions have merchants, merchants have locations, locations have regions. Without depth limiting, an attacker can craft a recursive query that traverses these relationships indefinitely, consuming massive server resources:
# Malicious deeply nested query
query {
user(id: "1") {
transactions {
merchant {
transactions {
user {
transactions {
merchant {
# ...infinite nesting
}
}
}
}
}
}
}
} Implement depth limiting using libraries like graphql-depth-limit (Node.js) or equivalent middleware. A depth limit of 5–7 is reasonable for most fintech schemas. Apply it as a validation rule before query execution, not after.
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7)]
}); 2. Query complexity analysis
Depth limiting alone isn't enough. A flat query requesting every field on a list of 10,000 transactions is shallow but expensive. Query complexity analysis assigns a cost to each field and resolver, then rejects queries that exceed a threshold:
# Flat but expensive query
query {
allTransactions(first: 10000) {
id
amount
status
merchant { name, address, category }
user { name, email, phone }
}
} Use graphql-query-complexity or Apollo Server's built-in cost analysis. Assign higher costs to fields that trigger database joins or external API calls. Set a maximum cost per query and return a clear error when exceeded. This prevents denial-of-service through resource-exhausting queries without limiting legitimate usage.
3. Disable introspection in production
GraphQL introspection lets clients query the schema itself — every type, field, argument, and relationship. In development, this powers tools like GraphiQL and Apollo Studio. In production, it hands attackers a complete map of your API.
// Disable introspection in Apollo Server
const server = new ApolloServer({
schema,
introspection: process.env.NODE_ENV !== 'production'
}); With introspection enabled, an attacker immediately discovers admin-only fields, internal mutations, deprecated but still functional endpoints, and the exact data model of your financial records. Disable it in production. If your frontend team needs schema awareness, use a schema registry or static schema files — not live introspection on production.
Introspection revealed admin mutations
During a GraphQL API assessment for a Nigerian digital banking platform, introspection was enabled in production. We discovered an adminUpdateBalance mutation that was undocumented but fully functional. It required authentication but no admin role check — any authenticated user could modify any account balance directly.
4. Field-level authorization
GraphQL's nested resolver pattern creates a subtle authorization problem. A user query might be properly authenticated, but a nested field — say user.transactions.merchant.revenue — might resolve data the requesting user shouldn't access. Each resolver in the chain needs its own authorization check.
Don't rely on schema design to enforce access control. A field that "only admins would query" is still queryable by anyone unless you enforce it in the resolver. Implement authorization at the resolver level using directives or middleware:
const resolvers = {
User: {
transactions: (parent, args, context) => {
// Verify the requesting user owns this user record
if (parent.id !== context.user.id && !context.user.isAdmin) {
throw new ForbiddenError('Not authorized');
}
return getTransactions(parent.id);
}
}
}; This is the GraphQL equivalent of BOLA/IDOR vulnerabilities in REST APIs. The shape of the problem is different (nested resolvers vs. endpoint paths), but the impact is identical: unauthorized data access.
5. Batching attacks
GraphQL supports query batching — sending multiple queries in a single HTTP request. An attacker can abuse this to bypass rate limiting (one HTTP request contains 1,000 login attempts) or to perform alias-based enumeration:
# Alias-based brute force in a single request
query {
a1: login(email: "target@email.com", password: "password1") { token }
a2: login(email: "target@email.com", password: "password2") { token }
a3: login(email: "target@email.com", password: "password3") { token }
# ...hundreds more aliases
} Defences: limit the number of operations per batched request, limit aliases per query, and apply rate limiting at the operation level (not the HTTP request level). Track login attempts by identity, not by IP or request count. See rate limiting for payment APIs for broader patterns.
6. Persisted queries
Instead of allowing arbitrary queries from clients, use persisted (or allowlisted) queries. During build time, extract all GraphQL queries from your frontend code, hash them, and register the hashes with your server. At runtime, clients send only the hash — the server looks up and executes the registered query.
This eliminates an entire class of attacks: injection via custom queries, resource exhaustion via crafted queries, and schema exploration. It also improves performance by reducing payload size. Apollo Server supports Automatic Persisted Queries (APQ) out of the box.
For fintech APIs with a defined set of client applications, persisted queries are the strongest defence. If you also have a public API for third-party integrations, you'll need to combine persisted queries with the other defences described above.
Running a GraphQL API for your fintech product? We test for depth attacks, authorization bypasses, and the full range of GraphQL-specific vulnerabilities.
Book a GraphQL security audit7. N+1 queries and auth bypass
The N+1 problem in GraphQL isn't just a performance issue — it can be a security issue. DataLoader batches resolver calls for efficiency, but if your authorization logic assumes resolvers execute individually, batching can bypass per-item checks. Ensure your DataLoader implementations include authorization in the batch function, not just the resolver that triggers the load.
Also verify that error handling in batch resolvers doesn't leak information. A partially authorized batch request should return null or an error for unauthorized items without revealing that the items exist (preventing enumeration).
GraphQL's flexibility is its security challenge
Every feature that makes GraphQL powerful — nested queries, introspection, batching, flexible field selection — is also an attack vector. Depth limiting, complexity analysis, disabled introspection, field-level auth, anti-batching controls, and persisted queries aren't optional hardening. They're the baseline for any GraphQL API handling financial data.
Related reading
Blog: BOLA API vulnerabilities · Secure your fintech API · Rate limiting for payment APIs
Guides: OWASP for fintech · Pentest tools and methodology · Web app pentesting
Services: API security · Penetration testing · Secure architecture review