From ac8f9bf65805093574d35ae09f5e43c6a4019e2b Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 8 Jun 2022 13:39:52 -0700 Subject: [PATCH] Warn on unconfigured `cache` (#6545) Issue a warning in production mode if neither the cache nor the APQ cache (persistedQueries.cache) are configured. We've provided a simple path to using a bounded cache via: #6536 The current default for AS3 is an unbounded in memory cache, which is susceptible to a DOS attack since APQs can fill up the server's memory with no limit. This warning provides an actionable recommendation to update their configuration in order to prevent this. --- CHANGELOG.md | 1 + .../apollo-server-core/src/ApolloServer.ts | 14 ++++ .../src/__tests__/ApolloServer.test.ts | 2 + .../src/__tests__/ApolloServer.test.ts | 2 + .../src/__tests__/ApolloServer.test.ts | 2 + .../src/ApolloServer.ts | 69 +++++++++++++++++++ .../src/__tests__/ApolloServer.test.ts | 2 + 7 files changed, 92 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b2bbce0e3..f30147dca13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The version headers in this history reflect the versions of Apollo Server itself - Remove dependency on `keyv`/`@apollo/utils.keyvadapter` in favor of a simple `Map`-backed cache which implements TTL [PR #6535](https://github.com/apollographql/apollo-server/pull/6535) - Add `cache: "bounded"` configuration option, allowing users to opt into bounded request cache (recommended) [PR #6536](https://github.com/apollographql/apollo-server/pull/6536) - Remove `apollo-server-caching` and `apollo-server-cache-*` packages [PR #6541](https://github.com/apollographql/apollo-server/pull/6541) +- Warn when APQ cache is unbounded in production (which is default) [PR #6545](https://github.com/apollographql/apollo-server/pull/6545) ## v3.8.2 diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 154cdef924e..5b527783a25 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -267,6 +267,20 @@ export class ApolloServerBase< if (!requestOptions.cache) { requestOptions.cache = new UnboundedCache(); + + if ( + !isDev && + (requestOptions.persistedQueries === undefined || + (requestOptions.persistedQueries && + !requestOptions.persistedQueries.cache)) + ) { + this.logger.warn( + 'Persisted queries are enabled and are using an unbounded cache. Your server' + + ' is vulnerable to denial of service attacks via memory exhaustion. ' + + 'Set `cache: "bounded"` or `persistedQueries: false` in your ApolloServer ' + + 'constructor, or see FIXME:DOCS for other alternatives.', + ); + } } if (requestOptions.persistedQueries !== false) { diff --git a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts index 1ab2d827175..aee3bcfd5ae 100644 --- a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts @@ -327,6 +327,7 @@ describe('apollo-server-express', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -356,6 +357,7 @@ describe('apollo-server-express', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); diff --git a/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts index d4069f51501..e7ecf681e49 100644 --- a/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-fastify/src/__tests__/ApolloServer.test.ts @@ -346,6 +346,7 @@ describe('apollo-server-fastify', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -375,6 +376,7 @@ describe('apollo-server-fastify', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); diff --git a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts index c1363b16837..bed25612708 100644 --- a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts @@ -350,6 +350,7 @@ describe('non-integration tests', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -379,6 +380,7 @@ describe('non-integration tests', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 90aedbaef29..bcfea09861f 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -268,6 +268,7 @@ export function testApolloServer( schema, stopOnTerminationSignals: false, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -287,6 +288,7 @@ export function testApolloServer( introspection: true, stopOnTerminationSignals: false, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -1730,6 +1732,7 @@ export function testApolloServer( }, stopOnTerminationSignals: false, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -1760,6 +1763,7 @@ export function testApolloServer( }, stopOnTerminationSignals: false, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -2296,6 +2300,71 @@ export function testApolloServer( expect(server['requestOptions'].cache).toBe(customCache); }); + it("warns in production mode when cache isn't configured and APQ isn't disabled", () => { + const mockLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + + new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + nodeEnv: 'production', + logger: mockLogger, + }); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringMatching( + /Persisted queries are enabled and are using an unbounded cache/, + ), + ); + }); + + it("doesn't warn about cache configuration if: not production mode, cache configured, APQ disabled, or APQ cache configured", () => { + const mockLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + + // dev mode + new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + nodeEnv: 'development', + logger: mockLogger, + }); + + // cache configured + new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + nodeEnv: 'production', + logger: mockLogger, + cache: 'bounded', + }); + + // APQ disabled + new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + nodeEnv: 'development', + logger: mockLogger, + persistedQueries: false, + }); + + // APQ cache configured + new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + nodeEnv: 'development', + logger: mockLogger, + persistedQueries: { + cache: {} as KeyValueCache, + }, + }); + + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); + it('basic caching', async () => { const typeDefs = gql` type Query { diff --git a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts index 49ca8df0cb3..65e213c5f78 100644 --- a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts @@ -316,6 +316,7 @@ describe('apollo-server-koa', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri }); @@ -345,6 +346,7 @@ describe('apollo-server-koa', () => { }, }, nodeEnv: 'production', + cache: 'bounded', }); const apolloFetch = createApolloFetch({ uri });