Skip to content

Commit

Permalink
Merge pull request from GHSA-68jh-rf6x-836f
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
trevor-scheer committed Jun 15, 2023
1 parent aeb511c commit 0adaf80
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 105 deletions.
14 changes: 14 additions & 0 deletions .changeset/pink-walls-train.md
@@ -0,0 +1,14 @@
---
'@apollo/server-integration-testsuite': patch
'@apollo/server': patch
---

Address Content Security Policy issues

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.

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.

Additionally, this change adds other applicable CSPs for the scripts, styles, images, manifest, and iframes that the landing pages load.

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).
1 change: 1 addition & 0 deletions cspell-dict.txt
Expand Up @@ -89,6 +89,7 @@ Hofmann
hrtime
htmls
httpcache
iframes
IHTTP
importability
instanceof
Expand Down
13 changes: 0 additions & 13 deletions docs/source/api/plugin/landing-pages.mdx
Expand Up @@ -460,19 +460,6 @@ The default value is `false`, in which case the landing page displays a basic `c

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

</td>
</tr>
<tr>
<td>

###### `precomputedNonce`

`string`
</td>
<td>

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.

</td>
</tr>

Expand Down
10 changes: 7 additions & 3 deletions docs/source/integrations/plugins-event-reference.mdx
Expand Up @@ -149,7 +149,7 @@ const server = new ApolloServer({

</MultiCodeBlock>

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.
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.

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

Expand All @@ -166,7 +166,11 @@ const server = new ApolloServer({
async serverWillStart() {
return {
async renderLandingPage() {
return { html: `<html><body>Welcome to your server!</body></html>` };
return {
async html() {
return `<html><body>Welcome to your server!</body></html>`;
},
};
},
};
},
Expand Down Expand Up @@ -354,7 +358,7 @@ This event is _not_ associated with your GraphQL server's _resolvers_. When this

> If the operation is anonymous (i.e., the operation is `query { ... }` instead of `query NamedQuery { ... }`), then `operationName` is `null`.
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).
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).

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.

Expand Down
26 changes: 25 additions & 1 deletion packages/integration-testsuite/src/apolloServerTests.ts
Expand Up @@ -2881,7 +2881,7 @@ export function defineIntegrationTestSuiteApolloServerTests(
}

function makeServerConfig(
htmls: string[],
htmls: (string | (() => Promise<string>))[],
): ApolloServerOptions<BaseContext> {
return {
typeDefs: 'type Query {x: ID}',
Expand Down Expand Up @@ -2967,6 +2967,30 @@ export function defineIntegrationTestSuiteApolloServerTests(
});
});

describe('async `html` function', () => {
it('returns a string', async () => {
url = (await createServer(makeServerConfig([async () => 'BAZ']))).url;
await get().expect(200, 'BAZ');
});
it('throws', async () => {
const logger = mockLogger();
url = (
await createServer({
...makeServerConfig([
async () => {
throw new Error('Landing page failed to render!');
},
]),
logger,
})
).url;
await get().expect(500);
expect(logger.error).toHaveBeenCalledWith(
'Landing page `html` function threw: Error: Landing page failed to render!',
);
});
});

// If the server was started in the background, then createServer does not
// throw.
options.serverIsStartedInBackground ||
Expand Down
15 changes: 14 additions & 1 deletion packages/server/src/ApolloServer.ts
Expand Up @@ -1016,11 +1016,24 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
runningServerState.landingPage &&
this.prefersHTML(httpGraphQLRequest)
) {
let renderedHtml;
if (typeof runningServerState.landingPage.html === 'string') {
renderedHtml = runningServerState.landingPage.html;
} else {
try {
renderedHtml = await runningServerState.landingPage.html();
} catch (maybeError: unknown) {
const error = ensureError(maybeError);
this.logger.error(`Landing page \`html\` function threw: ${error}`);
return this.errorResponse(error, httpGraphQLRequest);
}
}

return {
headers: new HeaderMap([['content-type', 'text/html']]),
body: {
kind: 'complete',
string: runningServerState.landingPage.html,
string: renderedHtml,
},
};
}
Expand Down
Expand Up @@ -41,22 +41,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
Apollo Explorer cannot be loaded; it appears that you might be offline.
</p>
</div>
<style>
<style nonce="nonce">
iframe {
background-color: white;
height: 100%;
width: 100%;
border: none;
}
#embeddableExplorer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
}
</style>
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
>
<div id="embeddableExplorer">
</div>
<script nonce="nonce"
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
>
</script>
<script nonce="nonce">
var endpointUrl = window.location.href;
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/server@4.0.0","runTelemetry":true};
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/server@4.0.0","runTelemetry":true,"allowDynamicStyles":false};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
Expand Down Expand Up @@ -84,22 +91,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
Apollo Explorer cannot be loaded; it appears that you might be offline.
</p>
</div>
<style>
<style nonce="nonce">
iframe {
background-color: white;
height: 100%;
width: 100%;
border: none;
}
#embeddableExplorer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
}
</style>
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
>
<div id="embeddableExplorer">
</div>
<script nonce="nonce"
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
>
</script>
<script nonce="nonce">
var endpointUrl = window.location.href;
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":true};
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":true,"allowDynamicStyles":false};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
Expand Down Expand Up @@ -128,22 +142,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
Apollo Explorer cannot be loaded; it appears that you might be offline.
</p>
</div>
<style>
<style nonce="nonce">
iframe {
background-color: white;
height: 100%;
width: 100%;
border: none;
}
#embeddableExplorer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
}
</style>
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
>
<div id="embeddableExplorer">
</div>
<script nonce="nonce"
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
>
</script>
<script nonce="nonce">
var endpointUrl = window.location.href;
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"collectionId":"12345","operationId":"abcdef","displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":true};
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"collectionId":"12345","operationId":"abcdef","displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":true,"allowDynamicStyles":false};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
Expand All @@ -170,22 +191,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
Apollo Explorer cannot be loaded; it appears that you might be offline.
</p>
</div>
<style>
<style nonce="nonce">
iframe {
background-color: white;
height: 100%;
width: 100%;
border: none;
}
#embeddableExplorer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
}
</style>
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
>
<div id="embeddableExplorer">
</div>
<script nonce="nonce"
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
>
</script>
<script nonce="nonce">
var endpointUrl = window.location.href;
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"displayOptions":{}},"persistExplorerState":false,"includeCookies":false,"runtime":"@apollo/server@4.0.0","runTelemetry":true};
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"displayOptions":{}},"persistExplorerState":false,"includeCookies":false,"runtime":"@apollo/server@4.0.0","runTelemetry":true,"allowDynamicStyles":false};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
Expand Down Expand Up @@ -215,22 +243,29 @@ describe('Embedded Explorer Landing Page Config HTML', () => {
Apollo Explorer cannot be loaded; it appears that you might be offline.
</p>
</div>
<style>
<style nonce="nonce">
iframe {
background-color: white;
height: 100%;
width: 100%;
border: none;
}
#embeddableExplorer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
}
</style>
<div style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
>
<div id="embeddableExplorer">
</div>
<script nonce="nonce"
src="https://embeddable-explorer.cdn.apollographql.com/_latest/embeddable-explorer.umd.production.min.js?runtime=%40apollo%2Fserver%404.0.0"
>
</script>
<script nonce="nonce">
var endpointUrl = window.location.href;
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":false};
var embeddedExplorerConfig = {"graphRef":"graph@current","target":"#embeddableExplorer","initialState":{"headers":{"authorization":"true"},"displayOptions":{}},"persistExplorerState":false,"includeCookies":true,"runtime":"@apollo/server@4.0.0","runTelemetry":false,"allowDynamicStyles":false};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
Expand Down

0 comments on commit 0adaf80

Please sign in to comment.