diff --git a/.nvmrc b/.nvmrc index 958b5a36e1f..dae199aecb1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14 +v12 diff --git a/CHANGELOG.md b/CHANGELOG.md index f44748d1a22..a8859ff0a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. With few exceptions, the format of the entry should follow convention (i.e., prefix with package name, use markdown `backtick formatting` for package names and code, suffix with a link to the change-set à la `[PR #YYY](https://link/pull/YYY)`, etc.). When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. +## v2.25.4 + +- ⚠️ **SECURITY**: If your server does not explicitly enable `graphql-upload` support via the `uploads` option to `new ApolloServer` and your schema does not use the `Upload` scalar (other than in its own definition), Apollo Server will not process the `multipart/form-data` requests sent by `graphql-upload` clients. This fixes a Cross-Site Request Forgery (CSRF) vulnerability where origins could cause browsers to execute mutations using a user's cookies even when those origins are not allowed by your CORS policy. If you *do* use uploads in your server, the vulnerability still exists with this version; you should instead upgrade to Apollo Server v3.7 and enable the CSRF prevention feature. (The AS3.7 CSRF prevention feature also protects against other forms of CSRF such as timing attacks against read-only query operations.) See [advisory GHSA-2p3c-p3qw-69r4](https://github.com/apollographql/apollo-server/security/advisories/GHSA-2p3c-p3qw-69r4) for more details. ## v2.25.3 diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index 75084f9d626..9754da9bca3 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -158,7 +158,7 @@ An executable GraphQL schema. You usually don't need to provide this value, beca This field is most commonly used with [Apollo Federation](https://www.apollographql.com/docs/federation/implementing-services/#generating-a-federated-schema), which uses a special `buildFederatedSchema` function to generate its schema. -Note that if you are using [file uploads](../data/file-uploads/), you need to add the `Upload` scalar to your schema manually before providing it here, as Apollo Server's automatic uploads integration does not apply if you use this particular option.. +Note that if you are using [file uploads](../data/file-uploads/), you need to add the `Upload` scalar to your schema manually before providing it here, as Apollo Server's automatic uploads integration does not apply if you use this particular option. (We do not recommend the use of file uploads in Apollo Server 2 for security reasons detailed on its page.) @@ -235,9 +235,11 @@ An object containing custom functions to use as additional [validation rules](ht -By default, Apollo Server 2 [integrates](../data/file-uploads/) an outdated version of [the `graphql-upload` npm module](https://www.npmjs.com/package/graphql-upload#graphql-upload) into your schema which does not support Node 14. If you provide an object here, it will be passed as the third `options` argument to that module's `processRequest` option. +By default, Apollo Server 2 [integrates](../data/file-uploads/) an outdated version of [the `graphql-upload` npm module](https://www.npmjs.com/package/graphql-upload#graphql-upload) into your schema which does not support Node 14 or newer. (As of v2.25.4, it is only enabled by default if you actually use the `Upload` scalar in your schema outside of its definition.) If you provide an object here, it will be passed as the third `options` argument to that module's `processRequest` option. -We recommend instead that you pass `false` here, which will disable Apollo Server's integration with `graphql-upload`, and instead integrate the latest version of that module directly. This integration will be removed in Apollo Server 3. +We recommend instead that you pass `false` here, which will disable Apollo Server's integration with `graphql-upload`. We then recommend that you find an alternate mechanism for accepting uploads in your app that does not involve `graphql-upload`, or that you upgrade to Apollo Server 3.7 and use its [CSRF prevention feature](https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf). + +If you must use the upload feature with Apollo Server 2, then you can do so on Node 14 by passing `false` here and directly integrating the latest version of `graphql-upload` directly. This will probably expose your server to CSRF mutation vulnerabilities so you must find some way of protecting yourself. (We recommend protecting yourself by upgrading and using our protection feature as described in the previous paragraph.) This integration has been removed in Apollo Server 3. diff --git a/docs/source/data/file-uploads.md b/docs/source/data/file-uploads.md index 77726851a71..65385bf5626 100644 --- a/docs/source/data/file-uploads.md +++ b/docs/source/data/file-uploads.md @@ -3,9 +3,11 @@ title: File uploads description: Enabling file uploads in Apollo Server --- +> **WARNING**: The file upload mechanism described in this file (which we removed in Apollo Server 3) inherently exposes your server to [CSRF mutation attacks](https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf). These attacks allow untrusted websites to ask users' browsers to send mutations to your Apollo Server which can execute even if your server's CORS security policy specifies that the origin in question should not be able to send that request. This is because the `multipart/form-data` content type that is parsed by the file upload feature is special-cased by browsers and can be sent in a POST request without the browser needing to "ask permission" via a "preflight" OPTIONS request. Attackers can run any mutation with your cookies, not just upload-specific mutations. Since Apollo Server v2.25.4, we no longer automatically enable this `multipart/form-data` parser unless you explicitly enable it with the `uploads` option to `new ApolloServer()` or if your schema uses the `Upload` scalar in it; in this case, your server will be protected from the CSRF mutation vulnerability. You can also pass `uploads: false` to `new ApolloServer()` in any version of Apollo Server 2 to ensure that this dangerous parser is disabled. If you actually use the upload feature (particularly if your app uses cookies for authentication), we highly encourage you to [upgrade to Apollo Server v3.7 or newer](https://www.apollographql.com/docs/apollo-server/migration/) and enable its [CSRF prevention feature](https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf), or remove the use of uploads from your server. + > Note: Apollo Server's built-in file upload mechanism is not fully supported in Node 14 and later, and it will be removed in Apollo Server 3. For details, [see below](#uploads-in-node-14-and-later). -For server integrations that support file uploads (e.g. Express, hapi, Koa), Apollo Server enables file uploads by default. To enable file uploads, reference the `Upload` type in the schema passed to the Apollo Server construction. +For server integrations that support file uploads (e.g. Express, hapi, Koa), Apollo Server enables file uploads by default if your schema references the `Upload` type. ```js const { ApolloServer, gql } = require('apollo-server'); diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 055c4b7fb44..eb11e29b7eb 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -20,6 +20,9 @@ import { FieldDefinitionNode, DocumentNode, print, + printSchema, + parse, + visit, } from 'graphql'; import resolvable, { Resolvable } from '@josephg/resolvable'; import { GraphQLExtension } from 'graphql-extensions'; @@ -119,6 +122,7 @@ type SchemaDerivedData = { // versions of operations in-memory, allowing subsequent parses/validates // on the same operation to be executed immediately. documentStore?: InMemoryLRUCache; + disableUploads: boolean; }; type ServerState = @@ -160,6 +164,7 @@ export class ApolloServerBase { protected subscriptionServerOptions?: SubscriptionServerOptions; protected uploadsConfig?: FileUploadOptions; + private disableUploadsIfSchemaDoesNotUseUploadScalar: boolean; // set by installSubscriptionHandlers. private subscriptionServer?: SubscriptionServer; @@ -284,6 +289,7 @@ export class ApolloServerBase { this.requestOptions = requestOptions as GraphQLServerOptions; + this.disableUploadsIfSchemaDoesNotUseUploadScalar = false; if (uploads !== false && !forbidUploadsForTesting) { if (this.supportsUploads()) { if (!runtimeSupportsUploads) { @@ -294,10 +300,15 @@ export class ApolloServerBase { ); } - if (uploads === true || typeof uploads === 'undefined') { + if (uploads === true) { this.uploadsConfig = {}; + warnAboutUploads(this.logger, false); + } else if (typeof uploads === 'undefined') { + this.uploadsConfig = {}; + this.disableUploadsIfSchemaDoesNotUseUploadScalar = true; } else { this.uploadsConfig = uploads; + warnAboutUploads(this.logger, false); } //This is here to check if uploads is requested without support. By //default we enable them if supported by the integration @@ -845,14 +856,55 @@ export class ApolloServerBase { // Initialize the document store. This cannot currently be disabled. const documentStore = this.initializeDocumentStore(); + // If `uploads` wasn't explicitly specified, we look to see if the schema + // uses the Upload scalar and if it doesn't, we disable the upload + // middleware. + let disableUploads = false; + if (this.disableUploadsIfSchemaDoesNotUseUploadScalar) { + // We're not going to stress too much about error handling + // in this patch to an old version of Apollo Server. If + // this crashes for anyone they can file a bug and in the + // meantime pass `uploads: true` or `uploads: false` explicitly + // to work around. + const ast = parse(printSchema(schema)); + + // Assume that we are going to disable it unless we find a use of Upload. + disableUploads = true; + + visit(ast, { + // This will find things like field arguments and input object fields + // (including if it's nested in list or non-null). Notably, it will + // *not* find the `scalar Upload` definition that we may have auto-added + // to the schema because ScalarTypeDefinitionNode has a "NameNode", not + // a "NamedTypeNode". + NamedType(node) { + if (node.name.value === 'Upload') { + disableUploads = false; + } + } + }) + + if (!disableUploads) { + warnAboutUploads(this.logger, true); + } + } + return { schema, schemaHash, extensions, documentStore, + disableUploads, }; } + // Let the middleware know dynamically whether we are disabling uploads. + // (this.uploadsConfig also needs to be true in order to enable uploads.) + public disableUploads(): boolean { + // If the server isn't started, we shouldn't be processing operations anyway. + return this.state.phase !== 'started' || this.state.schemaDerivedData.disableUploads; + } + public async stop() { // Calling stop more than once should have the same result as the first time. if (this.state.phase === 'stopped') { @@ -1353,3 +1405,16 @@ function printNodeFileUploadsMessage(logger: Logger) { ].join('\n'), ); } + +function warnAboutUploads(logger: Logger, implicit: boolean) { + logger.error([ + 'The third-party `graphql-upload` package is enabled in your server', + (implicit + ? 'because you use the `Upload` scalar in your schema.' + : 'because you explicitly enabled it with the `uploads` option.'), + 'This package is vulnerable to Cross-Site Request Forgery (CSRF) attacks.', + 'We recommend you either disable uploads if it is not a necessary part of', + 'your server, or upgrade to Apollo Server 3.7 and enable CSRF prevention.', + 'See https://go.apollo.dev/s/graphql-upload-csrf for more details.', + ].join('\n')) +} diff --git a/packages/apollo-server-express/src/ApolloServer.ts b/packages/apollo-server-express/src/ApolloServer.ts index c8f205da7b3..32dabe7f620 100644 --- a/packages/apollo-server-express/src/ApolloServer.ts +++ b/packages/apollo-server-express/src/ApolloServer.ts @@ -50,6 +50,7 @@ const fileUploadMiddleware = ( ) => { // Note: we use typeis directly instead of via req.is for connect support. if ( + !server.disableUploads() && typeof processFileUploads === 'function' && typeis(req, ['multipart/form-data']) ) { diff --git a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts index bced5f2909a..2b55b31aa01 100644 --- a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts @@ -558,6 +558,97 @@ describe('apollo-server-express', () => { if (error.code !== 'EPIPE') throw error; } }); + + it('multipart requests work when Upload in schema', async () => { + const { port } = await createServer({ + typeDefs: gql` + type Query { + f(f: Upload!): ID + } + `, + }); + + const body = new FormData(); + body.append( + 'operations',JSON.stringify({ query: '{__typename}', + }), + ); + body.append('map', '{}'); + + try { + const resolved = await fetch(`http://localhost:${port}/graphql`, { + method: 'POST', + body: body as any, + }); + const response = await resolved.json(); + expect(response).toEqual({data: {__typename: 'Query'}}); + } catch (error) { + // This error began appearing randomly and seems to be a dev dependency bug. + // https://github.com/jaydenseric/apollo-upload-server/blob/18ecdbc7a1f8b69ad51b4affbd986400033303d4/test.js#L39-L42 + if (error.code !== 'EPIPE') throw error; + } + }); + + it('multipart requests work when uploads: true is passed', async () => { + const { port } = await createServer({ + typeDefs: gql` + type Query { + f: ID + } + `, + uploads: true, + }); + + const body = new FormData(); + body.append( + 'operations',JSON.stringify({ query: '{__typename}', + }), + ); + body.append('map', '{}'); + + try { + const resolved = await fetch(`http://localhost:${port}/graphql`, { + method: 'POST', + body: body as any, + }); + const response = await resolved.json(); + expect(response).toEqual({data: {__typename: 'Query'}}); + } catch (error) { + // This error began appearing randomly and seems to be a dev dependency bug. + // https://github.com/jaydenseric/apollo-upload-server/blob/18ecdbc7a1f8b69ad51b4affbd986400033303d4/test.js#L39-L42 + if (error.code !== 'EPIPE') throw error; + } + }); + + it('multipart requests do not work when uploads unspecified and Upload is not used in the schema', async () => { + const { port } = await createServer({ + typeDefs: gql` + type Query { + f: ID + } + `, + }); + + const body = new FormData(); + body.append( + 'operations',JSON.stringify({ query: '{__typename}', + }), + ); + body.append('map', '{}'); + + try { + const resolved = await fetch(`http://localhost:${port}/graphql`, { + method: 'POST', + body: body as any, + }); + const response = await resolved.text(); + expect(response).toEqual("POST body missing. Did you forget use body-parser middleware?"); + } catch (error) { + // This error began appearing randomly and seems to be a dev dependency bug. + // https://github.com/jaydenseric/apollo-upload-server/blob/18ecdbc7a1f8b69ad51b4affbd986400033303d4/test.js#L39-L42 + if (error.code !== 'EPIPE') throw error; + } + }); }, ); diff --git a/packages/apollo-server-fastify/src/ApolloServer.ts b/packages/apollo-server-fastify/src/ApolloServer.ts index 59e3293420a..3a805176e04 100644 --- a/packages/apollo-server-fastify/src/ApolloServer.ts +++ b/packages/apollo-server-fastify/src/ApolloServer.ts @@ -41,6 +41,7 @@ const fileUploadMiddleware = ( done: (err: Error | null, body?: any) => void, ) => { if ( + !server.disableUploads() && (req.req as any)[kMultipart] && typeof processFileUploads === 'function' ) { diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts index cafd8fc7ed4..55fd2e58a5e 100644 --- a/packages/apollo-server-hapi/src/ApolloServer.ts +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -74,7 +74,7 @@ export class ApolloServer extends ApolloServerBase { return h.continue; } - if (this.uploadsConfig && typeof processFileUploads === 'function') { + if (this.uploadsConfig && !this.disableUploads() && typeof processFileUploads === 'function') { await handleFileUploads(this.uploadsConfig)(request); } diff --git a/packages/apollo-server-koa/src/ApolloServer.ts b/packages/apollo-server-koa/src/ApolloServer.ts index 3608744b52c..376e23e4e3c 100644 --- a/packages/apollo-server-koa/src/ApolloServer.ts +++ b/packages/apollo-server-koa/src/ApolloServer.ts @@ -36,7 +36,7 @@ const fileUploadMiddleware = ( uploadsConfig: FileUploadOptions, server: ApolloServerBase, ) => async (ctx: Koa.Context, next: Function) => { - if (typeis(ctx.req, ['multipart/form-data'])) { + if (!server.disableUploads() && typeis(ctx.req, ['multipart/form-data'])) { try { ctx.request.body = await processFileUploads( ctx.req, diff --git a/packages/apollo-server-lambda/src/ApolloServer.ts b/packages/apollo-server-lambda/src/ApolloServer.ts index 2d0a52dede9..201474c2dff 100644 --- a/packages/apollo-server-lambda/src/ApolloServer.ts +++ b/packages/apollo-server-lambda/src/ApolloServer.ts @@ -296,7 +296,7 @@ export class ApolloServer> > | undefined; - if (isMultipart && typeof processFileUploads === 'function') { + if (isMultipart && !this.disableUploads() && typeof processFileUploads === 'function') { const request = new FileUploadRequest() as IncomingMessage; request.push( Buffer.from( diff --git a/packages/apollo-server-micro/src/ApolloServer.ts b/packages/apollo-server-micro/src/ApolloServer.ts index 94b5d605309..5a333bb7e88 100644 --- a/packages/apollo-server-micro/src/ApolloServer.ts +++ b/packages/apollo-server-micro/src/ApolloServer.ts @@ -41,7 +41,7 @@ export class ApolloServer extends ApolloServerBase { return async (req, res) => { this.graphqlPath = path || '/graphql'; - if (typeof processFileUploads === 'function') { + if (!this.disableUploads() && typeof processFileUploads === 'function') { await this.handleFileUploads(req, res); }