BugForge - Daily - Sokudo
Daily - Sokudo
Vulnerabilities Covered
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
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}"}

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}"}

{"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.

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}"}

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.

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.

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.

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),finalizeChampionshipmutation, 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
prizeCodeare 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