graphql_find_hidden
Discover undocumented GraphQL fields by analyzing suggestion errors and probing for sensitive field names. This tool sends read-only queries to reveal hidden type information for security testing.
Instructions
Find hidden/undocumented fields on a GraphQL type using field suggestion errors. Sends queries with intentionally misspelled field names to trigger GraphQL's field suggestion feature, which reveals valid field names. Also tries common sensitive field names directly. Returns: {discovered_fields: [str], suggestion_results: [...], direct_probe_results: [...]}. Side effects: Read-only POST requests. Sends ~25 requests.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | GraphQL endpoint URL | |
| type_name | Yes | GraphQL type to probe for hidden fields, e.g. 'User', 'Post', 'BlogPost' | |
| known_field | No | A known field on this type to use in queries, e.g. 'id' or 'title' | id |
| query_name | No | Query name to use for fetching objects, e.g. 'getUser' or 'getBlogPost' | |
| query_arg | No | Query argument, e.g. 'id: 1' or 'slug: "test"' | |
| auth_header | No | Authorization header value | |
| auth_cookie | No | Session cookie |
Implementation Reference
- src/tools/graphql.ts:244-423 (handler)Handler implementation for 'graphql_find_hidden' tool. It probes a GraphQL endpoint by intentionally sending misspelled field names to trigger error suggestions, and also performs direct probes for known sensitive field names.
async ({ url, type_name, known_field, query_name, query_arg, auth_header, auth_cookie, }) => { requireTool("curl"); const queryGraphql = async ( graphqlQuery: string ): Promise<Record<string, unknown>> => { const curlArgs: string[] = [ "-sk", "-o", "-", "-w", "\n__META__%{http_code}", "-X", "POST", "-H", "Content-Type: application/json", "-d", JSON.stringify({ query: graphqlQuery }), ]; if (auth_header) { curlArgs.push("-H", `Authorization: ${auth_header}`); } if (auth_cookie) { curlArgs.push("-b", auth_cookie); } curlArgs.push(url); const res = await runCmd("curl", curlArgs); let body = res.stdout; const metaMarker = body.lastIndexOf("__META__"); if (metaMarker !== -1) { body = body.slice(0, metaMarker); } try { return JSON.parse(body) as Record<string, unknown>; } catch { return { raw: body.slice(0, 500) }; } }; // Build the query prefix let queryPrefix: string; if (query_name && query_arg) { queryPrefix = `${query_name}(${query_arg})`; } else if (query_name) { queryPrefix = query_name; } else { // Guess common query patterns queryPrefix = type_name[0].toLowerCase() + type_name.slice(1); } // Phase 1: Try misspelled fields to trigger suggestions const probePrefixes = [ "passwor", // triggers: password "secre", // triggers: secret "toke", // triggers: token "admi", // triggers: admin "emai", // triggers: email "phon", // triggers: phone "priva", // triggers: private "hidde", // triggers: hidden "intern", // triggers: internal "creat", // triggers: createdAt, createdBy "updat", // triggers: updatedAt "delet", // triggers: deleted, deletedAt "rol", // triggers: role "permissio", // triggers: permission ]; const suggestionResults: Array<{ probe: string; suggestions: string[]; }> = []; const discoveredFromSuggestions = new Set<string>(); for (const probe of probePrefixes) { const query = `{ ${queryPrefix} { ${known_field} ${probe} } }`; const result = await queryGraphql(query); const errors = (result.errors as Array<Record<string, unknown>> | undefined) ?? []; const suggestions: string[] = []; for (const error of errors) { const msg = (error.message as string) || ""; // Parse suggestions like: Did you mean "password"? const found = msg.match(/"([^"]+)"/g) ?? []; for (const f of found) { const fieldName = f.replace(/"/g, ""); if (fieldName !== probe && fieldName !== known_field) { suggestions.push(fieldName); discoveredFromSuggestions.add(fieldName); } } } if (suggestions.length > 0) { suggestionResults.push({ probe, suggestions }); } } // Phase 2: Direct probe common sensitive field names const sensitiveFields = [ "password", "passwordHash", "secret", "secretKey", "token", "apiKey", "apiToken", "accessToken", "refreshToken", "admin", "isAdmin", "role", "roles", "permissions", "email", "phone", "ssn", "creditCard", "privateKey", "privateField", "hidden", "internal", "deletedAt", "createdBy", "updatedBy", "postPassword", "hash", "salt", ]; const directResults: Array<Record<string, unknown>> = []; const discoveredDirect = new Set<string>(); for (const field of sensitiveFields) { const query = `{ ${queryPrefix} { ${known_field} ${field} } }`; const result = await queryGraphql(query); const errors = (result.errors as Array<Record<string, unknown>> | undefined) ?? []; const data = result.data; if (data && errors.length === 0) { // Field exists and returned data discoveredDirect.add(field); directResults.push({ field, exists: true, data_returned: true, value_snippet: String(data).slice(0, 200), }); } else if (errors.length > 0) { // Check if error is "cannot query field" (doesn't exist) vs auth error const errorMsg = ((errors[0].message as string) || "").toLowerCase(); if ( errorMsg.includes("cannot query field") || errorMsg.includes("unknown field") ) { // Field doesn't exist — skip } else if ( errorMsg.includes("not authorized") || errorMsg.includes("forbidden") ) { discoveredDirect.add(field); directResults.push({ field, exists: true, data_returned: false, note: "Field exists but access denied", }); } } } const allDiscovered = Array.from( new Set([...discoveredFromSuggestions, ...discoveredDirect]) ).sort(); const result = { type_name, discovered_fields: allDiscovered, suggestion_results: suggestionResults, direct_probe_results: directResults, hint: allDiscovered.length > 0 ? `Found ${allDiscovered.length} hidden/sensitive fields: ${JSON.stringify(allDiscovered)}` : "No hidden fields discovered. Type may have minimal fields or suggestions are disabled.", }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } ); - src/tools/graphql.ts:212-243 (registration)Registration of the 'graphql_find_hidden' tool with its input schema definition.
server.tool( "graphql_find_hidden", "Find hidden/undocumented fields on a GraphQL type using field suggestion errors. Sends queries with intentionally misspelled field names to trigger GraphQL's field suggestion feature, which reveals valid field names. Also tries common sensitive field names directly. Returns: {discovered_fields: [str], suggestion_results: [...], direct_probe_results: [...]}. Side effects: Read-only POST requests. Sends ~25 requests.", { url: z.string().describe("GraphQL endpoint URL"), type_name: z .string() .describe( "GraphQL type to probe for hidden fields, e.g. 'User', 'Post', 'BlogPost'" ), known_field: z .string() .describe( "A known field on this type to use in queries, e.g. 'id' or 'title'" ) .default("id"), query_name: z .string() .optional() .describe( "Query name to use for fetching objects, e.g. 'getUser' or 'getBlogPost'" ), query_arg: z .string() .optional() .describe("Query argument, e.g. 'id: 1' or 'slug: \"test\"'"), auth_header: z .string() .optional() .describe("Authorization header value"), auth_cookie: z.string().optional().describe("Session cookie"), },