agentsclimarketplace

Graphql idor via introspection leak

Skill ShulkwiSEC/bb-huge/skills/curated/graphql-idor-via-introspection-leak

bb-huge πŸ€— , Personal bug bounty findings hub and bug bounty orchestration for multiple agents

Install
npx -y skills add ShulkwiSEC/bb-huge --skill graphql-idor-via-introspection-leak

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 18 stars18 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.3 KB, as published. Nobody here has run it

GraphQL IDOR via Introspection Leak Hunting

What Is Broken and Why

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.

Key Signals

  • Introspection not disabled β€” __schema returns data in production
  • id arguments typed as ID! or String! with no documented ownership constraint
  • Objects expose sensitive fields (PII, tokens, internal metadata) retrievable by bare ID
  • App uses Relay global IDs (base64-encoded TypeName:uuid) β€” trivially enumerable
  • Error messages like "Not found" vs "Forbidden" reveal object existence (oracle)
  • Batching enabled β€” can enumerate hundreds of IDs in one request without rate limiting
  • Dual-stack architecture (REST + GraphQL) where REST has authz middleware but GraphQL resolvers were added later

Methodology

  1. Discover the endpoint β€” probe /graphql, /api/graphql, /v1/graphql, /gql
  2. Run introspection β€” dump the full schema
  3. Identify object types with ID args β€” look for queries/mutations accepting id, userId, resourceId, ownerId
  4. Create two test accounts β€” Account A (attacker) and Account B (victim)
  5. Grab a victim resource ID β€” note an object ID owned by Account B
  6. Query as attacker β€” from Account A's session, call the resolver with Account B's ID
  7. Test mutations too β€” attempt update, delete, transfer mutations with cross-account IDs
  8. Test unauthenticated β€” remove session token entirely; some resolvers skip auth at the GraphQL layer
  9. Confirm impact β€” verify you can read, modify, or delete data you don't own

Payloads & Tools

Full 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: "[email protected]") { 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.

Bypass Techniques

  • Alias batching β€” 100 ID lookups in one request, bypassing per-request rate limits
  • Type confusion β€” integers: try sequential enumeration; UUIDs: check for Relay base64 encoding
  • Fragment reuse β€” use fragments to reduce query size and evade WAF pattern matching
  • Mutation chaining β€” chain read + mutation atomically to exfiltrate and modify in one request
  • Variable injection β€” move IDs into GraphQL variables instead of inline literals to bypass naive input filters
  • Persisted queries β€” if the app supports persisted query IDs, find a stored query that skips authz
  • Field aliasing on introspection-blocked endpoints β€” try {a:__schema{...}} if unaliased introspection is blocked

Exploitation Scenarios

Scenario 1 β€” Invoice Read IDOR

Setup: 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.

Scenario 2 β€” Mutation-Based Account Takeover

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.

Scenario 3 β€” Unauthenticated Relay Node Leak

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.

False Positives

  • Introspection returns schema but all resolvers enforce ownership β€” confirm by actually crossing account boundaries
  • 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 leak
  • Relay node(id:) returns data but only for public/non-sensitive types (e.g. public posts)

Fix Patterns

// 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;
}
  • Disable introspection in production (or restrict to authenticated/internal users only)
  • Apply authorization at the resolver level, not just at the HTTP middleware layer
  • Never trust client-supplied IDs β€” always scope DB queries to the authenticated user's context
  • Use a dedicated authorization layer (e.g. graphql-shield, OPA) applied uniformly across all resolvers
  • Treat GraphQL as a separate attack surface from REST even when they share the same database

Related Skills

[[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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.