Install
$ agentstack add skill-hlsitechio-claude-skills-security-graphql-security ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
GraphQL Security Audit
Audit GraphQL APIs for vulnerabilities specific to the query language and protocol: complexity attacks, introspection leaks, field-level auth bypass, mutation gaps.
When this skill applies
- Reviewing GraphQL schemas and resolvers
- Auditing depth / complexity / cost limits
- Reviewing field-level authorization
- Checking introspection exposure in production
- Auditing persisted queries setup
- Reviewing GraphQL subscriptions and WebSocket auth
Use other skills for: REST API patterns (saas-security-pack/saas-api-security), backend framework auth wiring (nodejs-express-security, nestjs-security, fastapi-security), IDOR patterns generally (saas-security-pack/saas-code-security-review).
Workflow
Follow ../_shared/audit-workflow.md. GraphQL-specific notes below.
Phase 1: Stack detection
# Find the GraphQL server library
grep -E '"(apollo-server|@apollo/server|graphql-yoga|mercurius|express-graphql|@nestjs/graphql|graphql-tools)":' package.json
# Schema files
find . \( -name '*.graphql' -o -name '*.gql' -o -name 'schema.ts' \) -not -path '*/node_modules/*' | head -10
# Resolver locations
grep -rln 'Query:\|Mutation:\|Subscription:\|resolvers\s*=' src/ | head -10
Phase 2: Inventory
# Endpoint configuration
grep -rnE 'graphqlPath|graphqlMiddleware|playground|introspection' src/
# Auth context setup
grep -rn 'context:\s*\(' src/ | head
# Subscriptions (often a separate auth flow)
grep -rn 'Subscription\|subscribe\|WebSocketServer\|graphql-ws' src/
# Federation (gateway / subgraph patterns)
grep -rn '@apollo/gateway\|buildSubgraphSchema' src/
Phase 3: Detection — the checks
Introspection in production
- GQL-INTRO-1 Introspection disabled in production OR access-controlled.
``ts // Apollo Server 4 const server = new ApolloServer({ schema, introspection: process.env.NODE_ENV !== 'production', }); ``
- GQL-INTRO-2 Even if "disabled", schema can sometimes be inferred via error messages. Sanitize error responses (see GQL-ERR-1).
- GQL-INTRO-3 Persisted queries + introspection-disabled = strong stance; client doesn't need introspection if queries are pre-registered.
If introspection must stay on (internal tooling), gate by auth — only authenticated admins.
Query depth and complexity
GraphQL queries can recurse via cyclic schema references. Without limits, a single query can cost megabytes of work:
query Evil {
user(id: "1") {
friends { friends { friends { friends { ... } } } }
}
}
- GQL-DEPTH-1 Depth limit configured (typical: 10-15). Libraries:
graphql-depth-limit, built-in to graphql-yoga, configurable in Apollo. - GQL-COMP-1 Complexity / cost analysis configured. Libraries:
graphql-query-complexity, GraphQL Armor. - GQL-COMP-2 Per-field cost annotations applied to expensive fields (search, full-text, pagination over large sets).
- GQL-COMP-3 Alias attack mitigated: counting aliases toward complexity (
a: thing b: thing c: thingshould count as 3, not 1). - GQL-COMP-4 Directives like
@streamand@defer(newer specs) have their own cost accounting if enabled.
import { createComplexityLimitRule } from 'graphql-validation-complexity';
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(10),
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
});
GraphQL Armor combines all these defaults in one plugin and is the lowest-friction way to deploy them.
Field-level authorization
REST has one auth check per endpoint; GraphQL has one auth surface per field. The mistake is checking at the root query and trusting the rest.
- GQL-AZ-1 Resolvers for fields exposing other users' data check authorization on the parent object, not just the root.
``ts // BAD — root checks auth but users resolver doesn't const resolvers = { Query: { currentUser: requireAuth((parent, args, ctx) => ctx.user), }, User: { // Wide-open: anyone who reaches a User can ask for any field email: (parent) => parent.email, paymentMethods: (parent) => db.paymentMethods.findMany({ where: { userId: parent.id } }), }, }; ` An attacker who navigates to a User from elsewhere (e.g., via a comment's author` field) gets their email and payment methods.
- GQL-AZ-2 Use directives or middleware for declarative field authz:
```graphql directive @auth(requires: Role = USER) on FIELD_DEFINITION
type User { id: ID! displayName: String! email: String! @auth(requires: SELFORADMIN) paymentMethods: [PaymentMethod!]! @auth(requires: SELF) } `` Implement the directive to check ctx.user` against the parent object on every field.
- GQL-AZ-3 Tools: GraphQL Shield (rule-based authz), Apollo Connectors with auth context, custom middleware. Don't roll your own per-resolver if you have more than ~20 resolvers — the surface is too large to maintain manually.
Mutations
- GQL-MUT-1 Every mutation checks auth + authz. Sensitive mutations (delete account, change billing, admin actions) require additional verification (MFA, re-auth).
- GQL-MUT-2 Mutations don't trust input IDs — derive
userId/tenantIdfromctx, not from arguments. - GQL-MUT-3 Mutations idempotent or rate-limited where appropriate.
- GQL-MUT-4 Input validation via schema or in resolver (use zod / yup / ajv at the resolver boundary; GraphQL types don't validate ranges or formats).
Persisted queries
Persisted queries dramatically reduce attack surface: clients send a query hash, server looks up the actual query. Attackers can't send arbitrary queries.
- GQL-PQ-1 Persisted queries enabled for first-party clients (web, mobile).
- GQL-PQ-2 Public/external clients (third-party developers, partners) — either allow arbitrary queries with strict depth/complexity, or define a partner API with persisted queries.
- GQL-PQ-3 Persisted query registry write access restricted to CI/CD pipeline; not writeable from production.
- GQL-PQ-4 Apollo Persisted Query Manifest workflow or equivalent in use.
Batching abuse
# Single request, 1000 mutations
mutation BatchEvil {
m1: addCredit(userId: "self", amount: 1000)
m2: addCredit(userId: "self", amount: 1000)
m3: addCredit(userId: "self", amount: 1000)
# ...
}
- GQL-BAT-1 Limit alias count and operation count per request.
- GQL-BAT-2 Rate limit mutations server-side per user/tenant — within a single request and across requests.
- GQL-BAT-3 Idempotency keys for mutations that should not be repeatable.
Subscriptions
Subscriptions typically use WebSocket; auth model differs from regular HTTP:
- GQL-SUB-1 WebSocket connection initialization receives auth token (typically in
connection_params); validated before subscription is established. - GQL-SUB-2 Subscription resolvers re-check auth on every emit — long-running subscription with stale auth that was revoked but never re-checked = security hole.
- GQL-SUB-3 Subscription topic filters scoped to the authenticated user — never let a client subscribe to "all events" with a server-side filter that may have bugs.
Error messages
- GQL-ERR-1 Production errors don't reveal internal details (stack traces, DB schema, internal IDs).
- GQL-ERR-2 Apollo Server's
formatError/ yoga'smaskedErrorsconfigured to strip detail from non-known errors. - GQL-ERR-3 Error codes (
extensions.code) don't leak existence vs permission. Same advice as REST: 404 not 403 for "not authorized to read this resource you can't know exists". - GQL-ERR-4 Schema syntax errors in production don't echo back the query verbatim (could log it server-side though).
CSRF on GraphQL endpoints
The GraphQL endpoint /graphql accepts POST with application/json. Combined with CORS misconfig, this can be a CSRF vector for mutations.
- GQL-CSRF-1 GraphQL endpoint requires a non-form-submittable Content-Type (
application/json) — most servers do by default, but verify the server enforces this (Apollo Server 4 enforces by default; older versions or custom integrations may not). - GQL-CSRF-2 Apollo CSRF Prevention enabled (
csrfPrevention: true— default in v4). - GQL-CSRF-3 SameSite cookies for session — same as REST.
Federation / gateway
If using Apollo Federation or similar:
- GQL-FED-1 Subgraph endpoints not directly reachable from the public internet — only via the gateway.
- GQL-FED-2 Each subgraph applies auth/authz itself (don't trust the gateway to filter).
- GQL-FED-3 Federation context propagation (auth claims) signed or otherwise verifiable between gateway and subgraph.
Specific library notes
Apollo Server 4+:
- Defaults are good: introspection off in production via NODE_ENV, CSRF prevention on, landing page disabled in production.
- Use
ApolloServerPluginUsageReportingcarefully — it sends query data to Apollo Studio; ensure no PII in queries (or anonymize).
graphql-yoga:
- Built-in
maskedErrors,useResponseCache. VerifyuseGraphqlArmorplugin enabled for production. - Yoga 4+ includes GraphQL Armor by default.
Mercurius (Fastify):
graphiql: truein production = bad. Check the config.ide: 'graphiql' | 'playground' | false.
Hasura:
- Permissions configured per role per table — review every role's permissions.
- Webhook / JWT auth setup verified.
HASURA_GRAPHQL_ADMIN_SECRETonly used for admin operations, never exposed.
PostGraphile:
- Built on Postgres RLS for permission — see
saas-security-pack/supabase-security-audit/references/rls-patterns.md(same patterns).
Phase 4: Triage
Critical class examples:
- No depth/complexity limits → DoS via single query
- Introspection on in production → schema fully discovered by attacker
- Field-level auth missing on PII-bearing fields
- Mutation accepting userId argument without verifying caller
- Persisted queries advertised but bypass route accepting arbitrary queries
Phase 5: Report
Use ../_shared/findings-schema.md. Prefix IDs with GQL-.
References
references/field-level-auth.md— Patterns for declarative field authorization (directives, GraphQL Shield, middleware)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: hlsitechio
- Source: hlsitechio/claude-skills-security
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.