Skip to content

Commit 0adaf80

Browse files
Merge pull request from GHSA-68jh-rf6x-836f
* CSP fixes * Update snapshots * spell check * add a test that actually validates the whole point of this change :) * Apply new config option `allowDynamicStyling` for explorer/sandbox With the updated (more restrictive) CSP, the dynamic styling implemented by explorer/sandbox now triggers a CSP error. With this new option, we can disable the dynamic styles (and just apply them ourselves in our nonced inline styles). * oops wrong symbol * test cleanup * warn on precomputedNonce configuration
1 parent aeb511c commit 0adaf80

File tree

13 files changed

+324
-105
lines changed

13 files changed

+324
-105
lines changed

.changeset/pink-walls-train.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@apollo/server-integration-testsuite': patch
3+
'@apollo/server': patch
4+
---
5+
6+
Address Content Security Policy issues
7+
8+
The previous implementation of CSP nonces within the landing pages did not take full advantage of the security benefit of using them. Nonces should only be used once per request, whereas Apollo Server was generating one nonce and reusing it for the lifetime of the instance. The reuse of nonces degrades the security benefit of using them but does not pose a security risk on its own. The CSP provides a defense-in-depth measure against a _potential_ XSS, so in the absence of a _known_ XSS vulnerability there is likely no risk to the user.
9+
10+
The mentioned fix also coincidentally addresses an issue with using crypto functions on startup within Cloudflare Workers. Crypto functions are now called during requests only, which resolves the error that Cloudflare Workers were facing. A recent change introduced a `precomputedNonce` configuration option to mitigate this issue, but it was an incorrect approach given the nature of CSP nonces. This configuration option is now deprecated and should not be used for any reason since it suffers from the previously mentioned issue of reusing nonces.
11+
12+
Additionally, this change adds other applicable CSPs for the scripts, styles, images, manifest, and iframes that the landing pages load.
13+
14+
A final consequence of this change is an extension of the `renderLandingPage` plugin hook. This hook can now return an object with an `html` property which returns a `Promise<string>` in addition to a `string` (which was the only option before).

cspell-dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Hofmann
8989
hrtime
9090
htmls
9191
httpcache
92+
iframes
9293
IHTTP
9394
importability
9495
instanceof

docs/source/api/plugin/landing-pages.mdx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -460,19 +460,6 @@ The default value is `false`, in which case the landing page displays a basic `c
460460

461461
You can configure the Explorer embedded on your Apollo Server endpoint with display and functional options. For supported options, see [`embed` options](#embed-options).
462462

463-
</td>
464-
</tr>
465-
<tr>
466-
<td>
467-
468-
###### `precomputedNonce`
469-
470-
`string`
471-
</td>
472-
<td>
473-
474-
The landing page renders with a randomly generated nonce for security purposes. If you'd like to provide your own nonce, you can do so here. This is useful for Cloudflare Workers which can't perform random number generation on startup.
475-
476463
</td>
477464
</tr>
478465

docs/source/integrations/plugins-event-reference.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const server = new ApolloServer({
149149

150150
</MultiCodeBlock>
151151

152-
The handler should return an object with a string `html` field. The value of that field is served as HTML for any requests with `accept: text/html` headers.
152+
The handler should return an object with a string `html` property. The value of that property is served as HTML for any requests with `accept: text/html` headers. The `html` property can also be an `async` function that returns a string. This function is called for each landing page request.
153153

154154
For more landing page options, see [Changing the landing page](../workflow/build-run-queries/#changing-the-landing-page).
155155

@@ -166,7 +166,11 @@ const server = new ApolloServer({
166166
async serverWillStart() {
167167
return {
168168
async renderLandingPage() {
169-
return { html: `<html><body>Welcome to your server!</body></html>` };
169+
return {
170+
async html() {
171+
return `<html><body>Welcome to your server!</body></html>`;
172+
},
173+
};
170174
},
171175
};
172176
},
@@ -354,7 +358,7 @@ This event is _not_ associated with your GraphQL server's _resolvers_. When this
354358

355359
> If the operation is anonymous (i.e., the operation is `query { ... }` instead of `query NamedQuery { ... }`), then `operationName` is `null`.
356360
357-
If a `didResolveOperation` hook throws a [`GraphQLError`](../data/errors#custom-errors), that error is serialized and returned to the client with an HTTP status code of 500 unless [it specifies a different status code](../data/errors#setting-http-status-code-and-headers).
361+
If a `didResolveOperation` hook throws a [`GraphQLError`](../data/errors#custom-errors), that error is serialized and returned to the client with an HTTP status code of 500 unless [it specifies a different status code](../data/errors#setting-http-status-code-and-headers).
358362

359363
The `didResolveOperation` hook is a great spot to perform extra validation because it has access to the parsed and validated operation and the request-specific context (i.e., `contextValue`). Multiple plugins can run the `didResolveOperation` in parallel, but if more than one plugin throws, the client only receives a single error.
360364

packages/integration-testsuite/src/apolloServerTests.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2881,7 +2881,7 @@ export function defineIntegrationTestSuiteApolloServerTests(
28812881
}
28822882

28832883
function makeServerConfig(
2884-
htmls: string[],
2884+
htmls: (string | (() => Promise<string>))[],
28852885
): ApolloServerOptions<BaseContext> {
28862886
return {
28872887
typeDefs: 'type Query {x: ID}',
@@ -2967,6 +2967,30 @@ export function defineIntegrationTestSuiteApolloServerTests(
29672967
});
29682968
});
29692969

2970+
describe('async `html` function', () => {
2971+
it('returns a string', async () => {
2972+
url = (await createServer(makeServerConfig([async () => 'BAZ']))).url;
2973+
await get().expect(200, 'BAZ');
2974+
});
2975+
it('throws', async () => {
2976+
const logger = mockLogger();
2977+
url = (
2978+
await createServer({
2979+
...makeServerConfig([
2980+
async () => {
2981+
throw new Error('Landing page failed to render!');
2982+
},
2983+
]),
2984+
logger,
2985+
})
2986+
).url;
2987+
await get().expect(500);
2988+
expect(logger.error).toHaveBeenCalledWith(
2989+
'Landing page `html` function threw: Error: Landing page failed to render!',
2990+
);
2991+
});
2992+
});
2993+
29702994
// If the server was started in the background, then createServer does not
29712995
// throw.
29722996
options.serverIsStartedInBackground ||

packages/server/src/ApolloServer.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,11 +1016,24 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
10161016
runningServerState.landingPage &&
10171017
this.prefersHTML(httpGraphQLRequest)
10181018
) {
1019+
let renderedHtml;
1020+
if (typeof runningServerState.landingPage.html === 'string') {
1021+
renderedHtml = runningServerState.landingPage.html;
1022+
} else {
1023+
try {
1024+
renderedHtml = await runningServerState.landingPage.html();
1025+
} catch (maybeError: unknown) {
1026+
const error = ensureError(maybeError);
1027+
this.logger.error(`Landing page \`html\` function threw: ${error}`);
1028+
return this.errorResponse(error, httpGraphQLRequest);
1029+
}
1030+
}
1031+
10191032
return {
10201033
headers: new HeaderMap([['content-type', 'text/html']]),
10211034
body: {
10221035
kind: 'complete',
1023-
string: runningServerState.landingPage.html,
1036+
string: renderedHtml,
10241037
},
10251038
};
10261039
}

packages/server/src/__tests__/plugin/landingPage/getEmbeddedExplorerHTML.test.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
4141
Apollo Explorer cannot be loaded; it appears that you might be offline.
4242
</p>
4343
</div>
44-
<style>
44+
<style nonce="nonce">
4545
iframe {
4646
background-color: white;
47+
height: 100%;
48+
width: 100%;
49+
border: none;
50+
}
51+
#embeddableExplorer {
52+
width: 100vw;
53+
height: 100vh;
54+
position: absolute;
55+
top: 0;
4756
}
4857
</style>
49-
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
50-
id="embeddableExplorer"
51-
>
58+
<div id="embeddableExplorer">
5259
</div>
5360
<script nonce="nonce"
5461
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
5562
>
5663
</script>
5764
<script nonce="nonce">
5865
var endpointUrl = window.location.href;
59-
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"document":"query Test { id }","headers":{"authorization":"true"},"variables":{"option":{"a":"val","b":1,"c":true}},"displayOptions":{"showHeadersAndEnvVars":true,"docsPanelState":"open","theme":"light"}},"persistExplorerState":true,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true};
66+
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"document":"query Test { id }","headers":{"authorization":"true"},"variables":{"option":{"a":"val","b":1,"c":true}},"displayOptions":{"showHeadersAndEnvVars":true,"docsPanelState":"open","theme":"light"}},"persistExplorerState":true,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true,"allowDynamicStyles":false};
6067
new window.EmbeddedExplorer({
6168
...embeddedExplorerConfig,
6269
endpointUrl,
@@ -84,22 +91,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
8491
Apollo Explorer cannot be loaded; it appears that you might be offline.
8592
</p>
8693
</div>
87-
<style>
94+
<style nonce="nonce">
8895
iframe {
8996
background-color: white;
97+
height: 100%;
98+
width: 100%;
99+
border: none;
100+
}
101+
#embeddableExplorer {
102+
width: 100vw;
103+
height: 100vh;
104+
position: absolute;
105+
top: 0;
90106
}
91107
</style>
92-
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
93-
id="embeddableExplorer"
94-
>
108+
<div id="embeddableExplorer">
95109
</div>
96110
<script nonce="nonce"
97111
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
98112
>
99113
</script>
100114
<script nonce="nonce">
101115
var endpointUrl = window.location.href;
102-
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true};
116+
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true,"allowDynamicStyles":false};
103117
new window.EmbeddedExplorer({
104118
...embeddedExplorerConfig,
105119
endpointUrl,
@@ -128,22 +142,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
128142
Apollo Explorer cannot be loaded; it appears that you might be offline.
129143
</p>
130144
</div>
131-
<style>
145+
<style nonce="nonce">
132146
iframe {
133147
background-color: white;
148+
height: 100%;
149+
width: 100%;
150+
border: none;
151+
}
152+
#embeddableExplorer {
153+
width: 100vw;
154+
height: 100vh;
155+
position: absolute;
156+
top: 0;
134157
}
135158
</style>
136-
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
137-
id="embeddableExplorer"
138-
>
159+
<div id="embeddableExplorer">
139160
</div>
140161
<script nonce="nonce"
141162
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
142163
>
143164
</script>
144165
<script nonce="nonce">
145166
var endpointUrl = window.location.href;
146-
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"collectionId":"12345","operationId":"abcdef","displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true};
167+
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"collectionId":"12345","operationId":"abcdef","displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":true,"allowDynamicStyles":false};
147168
new window.EmbeddedExplorer({
148169
...embeddedExplorerConfig,
149170
endpointUrl,
@@ -170,22 +191,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
170191
Apollo Explorer cannot be loaded; it appears that you might be offline.
171192
</p>
172193
</div>
173-
<style>
194+
<style nonce="nonce">
174195
iframe {
175196
background-color: white;
197+
height: 100%;
198+
width: 100%;
199+
border: none;
200+
}
201+
#embeddableExplorer {
202+
width: 100vw;
203+
height: 100vh;
204+
position: absolute;
205+
top: 0;
176206
}
177207
</style>
178-
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
179-
id="embeddableExplorer"
180-
>
208+
<div id="embeddableExplorer">
181209
</div>
182210
<script nonce="nonce"
183211
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
184212
>
185213
</script>
186214
<script nonce="nonce">
187215
var endpointUrl = window.location.href;
188-
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"displayOptions":{}},"persistExplorerState":false,"includeCookies":false,"runtime":"@apollo/[email protected]","runTelemetry":true};
216+
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"displayOptions":{}},"persistExplorerState":false,"includeCookies":false,"runtime":"@apollo/[email protected]","runTelemetry":true,"allowDynamicStyles":false};
189217
new window.EmbeddedExplorer({
190218
...embeddedExplorerConfig,
191219
endpointUrl,
@@ -215,22 +243,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
215243
Apollo Explorer cannot be loaded; it appears that you might be offline.
216244
</p>
217245
</div>
218-
<style>
246+
<style nonce="nonce">
219247
iframe {
220248
background-color: white;
249+
height: 100%;
250+
width: 100%;
251+
border: none;
252+
}
253+
#embeddableExplorer {
254+
width: 100vw;
255+
height: 100vh;
256+
position: absolute;
257+
top: 0;
221258
}
222259
</style>
223-
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
224-
id="embeddableExplorer"
225-
>
260+
<div id="embeddableExplorer">
226261
</div>
227262
<script nonce="nonce"
228263
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
229264
>
230265
</script>
231266
<script nonce="nonce">
232267
var endpointUrl = window.location.href;
233-
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":false};
268+
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/[email protected]","runTelemetry":false,"allowDynamicStyles":false};
234269
new window.EmbeddedExplorer({
235270
...embeddedExplorerConfig,
236271
endpointUrl,

0 commit comments

Comments
 (0)