From 304982b0c6a2f91b06b4a852fe77e8ac70194187 Mon Sep 17 00:00:00 2001 From: CMLKevin Date: Mon, 23 Mar 2026 16:04:29 +0800 Subject: [PATCH] fix(graphql): use variables for type details query Closes #320 --- apps/graphql/src/tools/graphql.tools.test.ts | 43 ++++++++++++++++++++ apps/graphql/src/tools/graphql.tools.ts | 23 ++++++++--- apps/graphql/vitest.config.ts | 7 ++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 apps/graphql/src/tools/graphql.tools.test.ts create mode 100644 apps/graphql/vitest.config.ts diff --git a/apps/graphql/src/tools/graphql.tools.test.ts b/apps/graphql/src/tools/graphql.tools.test.ts new file mode 100644 index 00000000..c156d9f4 --- /dev/null +++ b/apps/graphql/src/tools/graphql.tools.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { fetchTypeDetails } from './graphql.tools' + +describe('fetchTypeDetails', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('sends the type name as a GraphQL variable instead of interpolating it into the query', async () => { + const maliciousTypeName = '") { name } } # injected' + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ + data: { + __type: { + name: 'safe', + kind: 'OBJECT', + description: null, + fields: [], + inputFields: [], + interfaces: [], + enumValues: [], + possibleTypes: [], + }, + }, + errors: null, + }), + statusText: 'OK', + } as Response) + + await fetchTypeDetails(maliciousTypeName, 'test-token') + + expect(fetchMock).toHaveBeenCalledTimes(1) + const [, init] = fetchMock.mock.calls[0] + const body = JSON.parse(String(init?.body)) + + expect(body.query).toContain('query TypeDetails($typeName: String!)') + expect(body.query).toContain('__type(name: $typeName)') + expect(body.query).not.toContain(maliciousTypeName) + expect(body.variables).toEqual({ typeName: maliciousTypeName }) + }) +}) diff --git a/apps/graphql/src/tools/graphql.tools.ts b/apps/graphql/src/tools/graphql.tools.ts index aea4f415..39557241 100644 --- a/apps/graphql/src/tools/graphql.tools.ts +++ b/apps/graphql/src/tools/graphql.tools.ts @@ -113,10 +113,13 @@ async function fetchSchemaOverview(apiToken: string): Promise { +export async function fetchTypeDetails( + typeName: string, + apiToken: string +): Promise { const typeDetailsQuery = ` - query TypeDetails { - __type(name: "${typeName}") { + query TypeDetails($typeName: String!) { + __type(name: $typeName) { name kind description @@ -174,7 +177,11 @@ async function fetchTypeDetails(typeName: string, apiToken: string): Promise(typeDetailsQuery, apiToken) + const response = await executeGraphQLQuery( + typeDetailsQuery, + { typeName }, + apiToken + ) return response } @@ -221,7 +228,11 @@ async function executeGraphQLRequest(query: string, apiToken: string): Promis * @param apiToken Cloudflare API token * @returns The query results */ -async function executeGraphQLQuery(query: string, variables: any, apiToken: string) { +async function executeGraphQLQuery( + query: string, + variables: Record, + apiToken: string +): Promise { // Clone the variables to avoid modifying the original const queryVariables = { ...variables } @@ -249,7 +260,7 @@ async function executeGraphQLQuery(query: string, variables: any, apiToken: stri console.warn(`GraphQL query errors: ${errorMessages}`) } - return result + return result as T } /** diff --git a/apps/graphql/vitest.config.ts b/apps/graphql/vitest.config.ts new file mode 100644 index 00000000..73984c92 --- /dev/null +++ b/apps/graphql/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + }, +})