Skip to content

Commit 2ebf97f

Browse files
Merge pull request #450 from AikidoSec/graphql
Don't discover GraphQL queries from server-side rendering
2 parents af0be03 + 6a46be9 commit 2ebf97f

File tree

5 files changed

+348
-27
lines changed

5 files changed

+348
-27
lines changed

library/sources/GraphQL.schema.test.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import * as t from "tap";
22
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting";
3-
import { runWithContext } from "../agent/Context";
3+
import { Context, getContext, runWithContext } from "../agent/Context";
44
import { GraphQL } from "./GraphQL";
55
import { Token } from "../agent/api/Token";
66
import { createTestAgent } from "../helpers/createTestAgent";
77

8-
function getTestContext() {
8+
function getTestContext(): Context {
99
return {
1010
remoteAddress: "::1",
1111
method: "POST",
1212
url: "http://localhost:4000/graphql",
1313
query: {},
14-
headers: {},
15-
body: {},
14+
headers: {
15+
"content-type": "application/json",
16+
},
17+
body: {
18+
query: '{ getFile(path: "/etc/bashrc") }',
19+
},
1620
cookies: {},
1721
routeParams: {},
1822
source: "express",
@@ -108,6 +112,29 @@ type Author {
108112
}
109113
});
110114

115+
await runWithContext(
116+
{
117+
remoteAddress: "::1",
118+
method: "POST",
119+
url: "http://localhost:4000/server-rendered-page",
120+
query: {},
121+
headers: {},
122+
body: {
123+
query: "this is not a graphql query",
124+
},
125+
cookies: {},
126+
routeParams: {},
127+
source: "express",
128+
route: "/server-rendered-page",
129+
},
130+
async () => {
131+
await query({ id: "1" });
132+
133+
// Route is registered at the end of the request
134+
agent.onRouteExecute(getContext()!);
135+
}
136+
);
137+
111138
await agent.flushStats(1000);
112139

113140
t.same(api.getEvents().length, 1);
@@ -134,6 +161,14 @@ type Author {
134161
apispec: {},
135162
graphQLSchema: dsl,
136163
},
164+
{
165+
method: "POST",
166+
path: "/server-rendered-page",
167+
hits: 1,
168+
graphql: undefined,
169+
apispec: {},
170+
graphQLSchema: undefined,
171+
},
137172
],
138173
});
139174
});

library/sources/GraphQL.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as t from "tap";
22
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting";
3-
import { getContext, runWithContext } from "../agent/Context";
3+
import { Context, getContext, runWithContext } from "../agent/Context";
44
import { GraphQL } from "./GraphQL";
55
import { Token } from "../agent/api/Token";
66
import { createTestAgent } from "../helpers/createTestAgent";
@@ -56,13 +56,17 @@ t.test("it works", async () => {
5656
const schema = buildSchema(`
5757
type Query {
5858
getFile(path: String): String
59+
anotherQuery: String
5960
}
6061
`);
6162

6263
const root = {
6364
getFile: ({ path }: { path: string }) => {
6465
return "file content";
6566
},
67+
anotherQuery: () => {
68+
return "another query";
69+
},
6670
};
6771

6872
const query = async (path: string, variableValues?: Record<string, any>) => {
@@ -96,5 +100,33 @@ t.test("it works", async () => {
96100
await query("/etc/bashrc");
97101
const result = await query("/etc/bashrc");
98102
t.same(result.errors![0].message, "You are rate limited by Zen.");
103+
104+
// With operation name
105+
t.same(
106+
await graphql({
107+
schema,
108+
source: `
109+
query getFile {
110+
getFile(path: "/etc/bashrc")
111+
}
112+
113+
query anotherQuery {
114+
anotherQuery
115+
}
116+
`,
117+
rootValue: root,
118+
variableValues: {},
119+
operationName: "anotherQuery",
120+
}),
121+
{
122+
data: { anotherQuery: "another query" },
123+
}
124+
);
125+
});
126+
127+
// Empty context
128+
await runWithContext({} as Context, async () => {
129+
const response = await query("/etc/bashrc");
130+
t.same(response, { data: { getFile: "file content" } });
99131
});
100132
});

library/sources/GraphQL.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
/* eslint-disable prefer-rest-params */
22
import { Agent } from "../agent/Agent";
3-
import { getContext, updateContext } from "../agent/Context";
3+
import { Context, getContext, updateContext } from "../agent/Context";
44
import { Hooks } from "../agent/hooks/Hooks";
55
import { WrapPackageInfo } from "../agent/hooks/WrapPackageInfo";
66
import { Wrapper } from "../agent/Wrapper";
77
import type { ExecutionArgs } from "graphql/execution/execute";
88
import { isPlainObject } from "../helpers/isPlainObject";
99
import { extractInputsFromDocument } from "./graphql/extractInputsFromDocument";
1010
import { extractTopLevelFieldsFromDocument } from "./graphql/extractTopLevelFieldsFromDocument";
11+
import { isGraphQLOverHTTP } from "./graphql/isGraphQLOverHTTP";
1112
import { shouldRateLimitOperation } from "./graphql/shouldRateLimitOperation";
1213
import { wrapExport } from "../agent/hooks/wrapExport";
1314

1415
export class GraphQL implements Wrapper {
1516
private graphqlModule: typeof import("graphql") | undefined;
1617

1718
private discoverGraphQLSchema(
18-
method: string,
19-
route: string,
19+
context: Context,
2020
executeArgs: ExecutionArgs,
2121
agent: Agent
2222
) {
@@ -28,31 +28,38 @@ export class GraphQL implements Wrapper {
2828
return;
2929
}
3030

31-
if (!agent.hasGraphQLSchema(method, route)) {
31+
if (!context.method || !context.route) {
32+
return;
33+
}
34+
35+
if (!agent.hasGraphQLSchema(context.method, context.route)) {
3236
try {
3337
const schema = this.graphqlModule.printSchema(executeArgs.schema);
34-
agent.onGraphQLSchema(method, route, schema);
38+
agent.onGraphQLSchema(context.method, context.route, schema);
3539
} catch (e) {
3640
// Ignore errors
3741
}
3842
}
3943
}
4044

4145
private discoverGraphQLQueryFields(
42-
method: string,
43-
route: string,
46+
context: Context,
4447
executeArgs: ExecutionArgs,
4548
agent: Agent
4649
) {
50+
if (!context.method || !context.route) {
51+
return;
52+
}
53+
4754
const topLevelFields = extractTopLevelFieldsFromDocument(
4855
executeArgs.document,
4956
executeArgs.operationName ? executeArgs.operationName : undefined
5057
);
5158

5259
if (topLevelFields) {
5360
agent.onGraphQLExecute(
54-
method,
55-
route,
61+
context.method,
62+
context.route,
5663
topLevelFields.type,
5764
topLevelFields.fields.map((field) => field.name.value)
5865
);
@@ -76,19 +83,16 @@ export class GraphQL implements Wrapper {
7683
return;
7784
}
7885

79-
if (context.method && context.route) {
80-
this.discoverGraphQLSchema(
81-
context.method,
82-
context.route,
83-
executeArgs,
84-
agent
85-
);
86-
this.discoverGraphQLQueryFields(
87-
context.method,
88-
context.route,
89-
executeArgs,
90-
agent
91-
);
86+
if (
87+
context &&
88+
context.method &&
89+
context.route &&
90+
isGraphQLOverHTTP(context)
91+
) {
92+
// We only want to discover GraphQL over HTTP
93+
// We should ignore queries coming from a GraphQL client in SSR mode
94+
this.discoverGraphQLSchema(context, executeArgs, agent);
95+
this.discoverGraphQLQueryFields(context, executeArgs, agent);
9296
}
9397

9498
const userInputs = extractInputsFromDocument(

0 commit comments

Comments
 (0)