Skip to content

Commit b822810

Browse files
authored
feat(wobe-graphql-yoga): add security options (#57)
1 parent ee48efb commit b822810

4 files changed

Lines changed: 402 additions & 32 deletions

File tree

packages/wobe-graphql-apollo/src/index.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,181 @@ import getPort from 'get-port'
44
import { WobeGraphqlApolloPlugin } from '.'
55

66
describe('Wobe GraphQL Apollo plugin', () => {
7+
it('should reject GET requests by default', async () => {
8+
const port = await getPort()
9+
10+
const wobe = new Wobe()
11+
12+
await wobe.usePlugin(
13+
await WobeGraphqlApolloPlugin({
14+
options: {
15+
typeDefs: `#graphql
16+
type Query {
17+
hello: String
18+
}
19+
`,
20+
resolvers: {
21+
Query: {
22+
hello: () => 'Hello from Apollo!',
23+
},
24+
},
25+
},
26+
}),
27+
)
28+
29+
wobe.listen(port)
30+
31+
const res = await fetch(
32+
`http://127.0.0.1:${port}/graphql?query=${encodeURIComponent(`
33+
query { hello }
34+
`)}`,
35+
)
36+
37+
expect(res.status).toBeGreaterThanOrEqual(400)
38+
39+
wobe.stop()
40+
})
41+
42+
it('should allow GET requests when explicitly enabled', async () => {
43+
const port = await getPort()
44+
45+
const wobe = new Wobe()
46+
47+
await wobe.usePlugin(
48+
await WobeGraphqlApolloPlugin({
49+
allowGetRequests: true,
50+
options: {
51+
typeDefs: `#graphql
52+
type Query {
53+
hello: String
54+
}
55+
`,
56+
resolvers: {
57+
Query: {
58+
hello: () => 'Hello from Apollo!',
59+
},
60+
},
61+
},
62+
}),
63+
)
64+
65+
wobe.listen(port)
66+
67+
const res = await fetch(
68+
`http://127.0.0.1:${port}/graphql?query=${encodeURIComponent(`
69+
query { hello }
70+
`)}`,
71+
)
72+
73+
expect(res.status).toBe(200)
74+
expect(await res.json()).toEqual({
75+
data: { hello: 'Hello from Apollo!' },
76+
})
77+
78+
wobe.stop()
79+
})
80+
81+
it('should disable introspection and landing page in production by default', async () => {
82+
const port = await getPort()
83+
84+
const wobe = new Wobe()
85+
86+
await wobe.usePlugin(
87+
await WobeGraphqlApolloPlugin({
88+
isProduction: true,
89+
allowGetRequests: true,
90+
options: {
91+
typeDefs: `#graphql
92+
type Query {
93+
hello: String
94+
}
95+
`,
96+
resolvers: {
97+
Query: {
98+
hello: () => 'Hello from Apollo!',
99+
},
100+
},
101+
},
102+
}),
103+
)
104+
105+
wobe.listen(port)
106+
107+
const resLanding = await fetch(`http://127.0.0.1:${port}/graphql`)
108+
expect(resLanding.status).toBeGreaterThanOrEqual(400)
109+
110+
const res = await fetch(`http://127.0.0.1:${port}/graphql`, {
111+
method: 'POST',
112+
headers: {
113+
'Content-Type': 'application/json',
114+
},
115+
body: JSON.stringify({
116+
query: `
117+
query IntrospectionQuery {
118+
__schema { queryType { name } }
119+
}
120+
`,
121+
}),
122+
})
123+
124+
const body = await res.json()
125+
126+
expect(res.status).toBe(400)
127+
expect(body.errors?.[0]?.message?.toLowerCase()).toContain(
128+
'introspection',
129+
)
130+
131+
wobe.stop()
132+
})
133+
134+
it('should allow introspection when explicitly enabled in production', async () => {
135+
const port = await getPort()
136+
137+
const wobe = new Wobe()
138+
139+
await wobe.usePlugin(
140+
await WobeGraphqlApolloPlugin({
141+
isProduction: true,
142+
allowIntrospection: true,
143+
options: {
144+
typeDefs: `#graphql
145+
type Query {
146+
hello: String
147+
}
148+
`,
149+
resolvers: {
150+
Query: {
151+
hello: () => 'Hello from Apollo!',
152+
},
153+
},
154+
},
155+
}),
156+
)
157+
158+
wobe.listen(port)
159+
160+
const res = await fetch(`http://127.0.0.1:${port}/graphql`, {
161+
method: 'POST',
162+
headers: {
163+
'Content-Type': 'application/json',
164+
},
165+
body: JSON.stringify({
166+
query: `
167+
query IntrospectionQuery {
168+
__schema { queryType { name } }
169+
}
170+
`,
171+
}),
172+
})
173+
174+
const body = await res.json()
175+
176+
expect(res.status).toBe(200)
177+
expect(body.data?.__schema?.queryType?.name).toBeDefined()
178+
179+
wobe.stop()
180+
})
181+
7182
it('should have custom wobe context in graphql context with record', async () => {
8183
const port = await getPort()
9184

packages/wobe-graphql-apollo/src/index.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { ApolloServer, type ApolloServerOptions } from '@apollo/server'
2-
import {
3-
ApolloServerPluginLandingPageLocalDefault,
4-
ApolloServerPluginLandingPageProductionDefault,
5-
} from '@apollo/server/plugin/landingPage/default'
2+
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
63
import type {
74
Wobe,
85
MaybePromise,
@@ -22,6 +19,9 @@ export interface GraphQLApolloPluginOptions {
2219
resolve: () => Promise<Response>,
2320
res: WobeResponse,
2421
) => Promise<Response>
22+
allowGetRequests?: boolean
23+
isProduction?: boolean
24+
allowIntrospection?: boolean
2525
}
2626

2727
export const WobeGraphqlApolloPlugin = async ({
@@ -30,23 +30,30 @@ export const WobeGraphqlApolloPlugin = async ({
3030
graphqlMiddleware,
3131
context: apolloContext,
3232
isProduction,
33+
allowGetRequests = false,
34+
allowIntrospection,
3335
}: {
3436
options: ApolloServerOptions<any>
3537
graphqlEndpoint?: string
3638
context?: GraphQLApolloContext
3739
isProduction?: boolean
3840
} & GraphQLApolloPluginOptions): Promise<WobePlugin> => {
41+
const introspection =
42+
options.introspection ??
43+
(allowIntrospection === true ? true : isProduction ? false : true)
44+
3945
const server = new ApolloServer({
4046
...options,
47+
introspection,
4148
plugins: [
4249
...(options?.plugins || []),
43-
isProduction
44-
? ApolloServerPluginLandingPageProductionDefault({
45-
footer: false,
46-
})
47-
: ApolloServerPluginLandingPageLocalDefault({
48-
footer: false,
49-
}),
50+
...(isProduction
51+
? []
52+
: [
53+
ApolloServerPluginLandingPageLocalDefault({
54+
footer: false,
55+
}),
56+
]),
5057
],
5158
})
5259

@@ -108,20 +115,22 @@ export const WobeGraphqlApolloPlugin = async ({
108115
}, context.res)
109116
}
110117

111-
wobe.get(graphqlEndpoint, async (context) => {
112-
const response = await getResponse(context)
118+
if (allowGetRequests) {
119+
wobe.get(graphqlEndpoint, async (context) => {
120+
const response = await getResponse(context)
113121

114-
for (const [key, value] of context.res.headers.entries()) {
115-
if (key === 'set-cookie') {
116-
response.headers.append('set-cookie', value)
117-
continue
118-
}
122+
for (const [key, value] of context.res.headers.entries()) {
123+
if (key === 'set-cookie') {
124+
response.headers.append('set-cookie', value)
125+
continue
126+
}
119127

120-
response.headers.set(key, value)
121-
}
128+
response.headers.set(key, value)
129+
}
122130

123-
return response
124-
})
131+
return response
132+
})
133+
}
125134

126135
wobe.post(graphqlEndpoint, async (context) => {
127136
const response = await getResponse(context)

0 commit comments

Comments
 (0)