From 928bd9c8cc3321205adc45b79e9fc13bffccd336 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 9 Jun 2025 20:07:29 -0400 Subject: [PATCH 01/11] add guide on file uploads --- src/pages/learn/_meta.ts | 1 + src/pages/learn/file-uploads.mdx | 183 +++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 src/pages/learn/file-uploads.mdx diff --git a/src/pages/learn/_meta.ts b/src/pages/learn/_meta.ts index ded3ef9830..91970be179 100644 --- a/src/pages/learn/_meta.ts +++ b/src/pages/learn/_meta.ts @@ -19,6 +19,7 @@ export default { "best-practices": "", "thinking-in-graphs": "", "serving-over-http": "", + "file-uploads": "", authorization: "", pagination: "", "schema-design": "Schema Design", diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx new file mode 100644 index 0000000000..7ca8deca63 --- /dev/null +++ b/src/pages/learn/file-uploads.mdx @@ -0,0 +1,183 @@ +# Handling File Uploads + +GraphQL doesn't natively support file uploads. The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic +and historically assumed `application/json`, but the evolving [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/) +introduces support for additional media types: `application/graphql-response+json`. + +Since uploading files typically requires `multipart/form-data`, adding upload capabilities still +means extending the HTTP layer yourself. This guide explains how to handle file uploads using +[`graphql-http`](https://github.com/graphql/graphql-http), a minimal, spec-compliant GraphQL server implementation for JavaScript. + +## Why file uploads require extra work + +A standard GraphQL request sends a query or mutation and optional variables as JSON. But file +uploads require binary data, which JSON can't represent. Instead, clients typically use +`multipart/form-data`, the same encoding used for HTML file forms. This format is incompatible +with how GraphQL servers like `graphql-http` handle requests by default. + +To bridge this gap, the GraphQL community developed a convention: the [GraphQL multipart +request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). This +approach allows files to be uploaded as part of a GraphQL mutation, with the server handling the +`multipart/form-data` payload and injecting the uploaded file into the appropriate variable. + +## The multipart upload format + +The multipart spec defines a three-part request format: + +- `operations`: A JSON string representing the GraphQL operation +- `map`: A JSON object that maps file field name to variable paths +- One or more files: Attached to the form using the same field names referenced in the `map` + +### Example + +```graphql +mutation UploadFile($file: Upload!) { + uploadFile(file: $file) { + filename + mimetype + } +} +``` + +And the corresponding `map` field: + +```json +{ + "0": ["variables.file"] +} +``` + +The server is responsible for parsing the multipart body, interpreting the `map`, and replacing +variable paths with the corresponding file streams. + +## Implementing uploads with graphql-http + +The `graphql-http` package doesn’t handle multipart requests out of the box. To support file +uploads, you’ll need to: + +1. Parse the multipart form request. +2. Map the uploaded file(s) to GraphQL variables. +3. Inject those into the request body before passing it to `createHandler()`. + +Here's how to do it in an Express-based server using JavaScript and the [`busboy`](https://www.npmjs.com/package/busboy), +a popular library for parsing `multipart/form-data`. + +### Example: Express + graphql-http + busboy + +```js +import express from 'express'; +import busboy from 'busboy'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { schema } from './schema.js'; + +const app = express(); + +app.post('/graphql', (req, res, next) => { + const contentType = req.headers['content-type'] || ''; + + if (contentType.startsWith('multipart/form-data')) { + const bb = busboy({ headers: req.headers }); + let operations, map; + const files = {}; + + bb.on('field', (name, val) => { + if (name === 'operations') operations = JSON.parse(val); + else if (name === 'map') map = JSON.parse(val); + }); + + bb.on('file', (fieldname, file, { filename, mimeType }) => { + files[fieldname] = { file, filename, mimeType }; + }); + + bb.on('close', () => { + for (const [key, paths] of Object.entries(map)) { + for (const path of paths) { + const keys = path.split('.'); + let target = operations; + while (keys.length > 1) target = target[keys.shift()]; + target[keys[0]] = files[key].file; + } + } + req.body = operations; + next(); + }); + + req.pipe(bb); + } else { + next(); + } +}, createHandler({ schema })); + +app.listen(4000); +``` + +This example: + +- Parses `multipart/form-data` uploads. +- Extracts GraphQL query and variables from the `operations` field. +- Inserts file streams in place of `Upload` variables. +- Passes the modified request to `graphql-http`. + +## Defining the upload scalar + +The GraphQL schema must include a custom scalar type for uploaded files: + +```graphql +scalar Upload + +extend type Mutation { + uploadFile(file: Upload!): FileMetadata +} + +type FileMetadata { + filename: String! + mimetype: String! +} +``` + +In your resolvers, treat `file` as a readable stream: + +```js +export const resolvers = { + Upload: GraphQLScalarType, // implement as needed, or treat as opaque in resolver + Mutation: { + uploadFile: async (_, { file }) => { + const chunks = []; + for await (const chunk of file) { + chunks.push(chunk); + } + // process or store the file as needed + return { + filename: 'uploaded-file.txt', + mimetype: 'text/plain', + }; + } + } +}; +``` + +You can define `Upload` as a passthrough scalar if your server middleware already +handles file parsing: + +```js +import { GraphQLScalarType } from 'graphql'; + +export const Upload = new GraphQLScalarType({ + name: 'Upload', + serialize: () => { throw new Error('Upload serialization unsupported'); }, + parseValue: value => value, + parseLiteral: () => { throw new Error('Upload literals unsupported'); } +}); +``` + +## Best practices + +- Streaming: Don’t read entire files into memory. Instead, stream files to disk or an external +storage service. This reduces memory pressure and improves +scalability. +- Security: Always validate file types, restrict maximum file sizes, and sanitize filenames to prevent +path traversal or injection vulnerabilities. +- Alternatives: For large files or more scalable architectures, consider using pre-signed URLs +with an object storage service like S3. The client uploads the file directly, and the GraphQL +mutation receives the file URL instead. +- Client support: Use a client library that supports the GraphQL multipart request specification. From c61bfc0d854ed272eb9a1ebda8c3890f5e968eca Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 19 Jun 2025 21:50:50 -0400 Subject: [PATCH 02/11] rewrite based on feedback --- src/pages/learn/file-uploads.mdx | 173 ++++++++++--------------------- 1 file changed, 55 insertions(+), 118 deletions(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 7ca8deca63..68d631d62b 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -1,68 +1,76 @@ -# Handling File Uploads +# Handling File Uploads in GraphQL -GraphQL doesn't natively support file uploads. The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic -and historically assumed `application/json`, but the evolving [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/) -introduces support for additional media types: `application/graphql-response+json`. +GraphQL was not designed with file uploads in mind. While it’s technically possible to implement them, doing so requires +extending the transport layer and introduces several risks, both in security and reliability. -Since uploading files typically requires `multipart/form-data`, adding upload capabilities still -means extending the HTTP layer yourself. This guide explains how to handle file uploads using -[`graphql-http`](https://github.com/graphql/graphql-http), a minimal, spec-compliant GraphQL server implementation for JavaScript. +This guide explains why file uploads via GraphQL are problematic and presents safer alternatives. -## Why file uploads require extra work +## Why uploads are challenging -A standard GraphQL request sends a query or mutation and optional variables as JSON. But file -uploads require binary data, which JSON can't represent. Instead, clients typically use -`multipart/form-data`, the same encoding used for HTML file forms. This format is incompatible -with how GraphQL servers like `graphql-http` handle requests by default. +The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic and assumes requests are encoded as JSON. +File uploads, by contrast, require `multipart/form-data` encoding to transfer binary data—something JSON can’t handle. -To bridge this gap, the GraphQL community developed a convention: the [GraphQL multipart -request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). This -approach allows files to be uploaded as part of a GraphQL mutation, with the server handling the -`multipart/form-data` payload and injecting the uploaded file into the appropriate variable. +Supporting uploads over GraphQL usually involves adopting community conventions, like the +[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). While useful in some +environments, these solutions often introduce complexity, fragility, and security risks. -## The multipart upload format +## Risks to be aware of -The multipart spec defines a three-part request format: +### Memory exhaustion from repeated variables -- `operations`: A JSON string representing the GraphQL operation -- `map`: A JSON object that maps file field name to variable paths -- One or more files: Attached to the form using the same field names referenced in the `map` +GraphQL operations allow the same variable to be referenced multiple times. If a file upload variable is used more than +once, its stream may be consumed multiple times—or worse, not at all. This can lead to unpredictable behavior or denial of service (DoS). -### Example +### Stream leaks on failed operations -```graphql -mutation UploadFile($file: Upload!) { - uploadFile(file: $file) { - filename - mimetype - } -} -``` +GraphQL executes in phases: validation, then execution. If an error occurs during validation or authorization, your +server might never reach the resolver that consumes a file stream. If file streams are left unconsumed, memory usage can +spike, potentially exhausting server resources. -And the corresponding `map` field: +### Cross-Site Request Forgery (CSRF) -```json -{ - "0": ["variables.file"] -} -``` +`multipart/form-data` is classified as a “simple” request by CORS and does not trigger preflight checks. Without strict CSRF +protections, malicious sites may be able to upload files on behalf of unsuspecting users. + +### Oversized or excess payloads + +Attackers can upload arbitrarily large files or extra files not referenced in the GraphQL operation. If your server accepts and +buffers these files in memory, you may face reliability issues or be vulnerable to resource exhaustion. + +### Untrusted file metadata + +Uploaded file names, MIME types, and even contents are arbitrary and should be treated as untrusted input. Failing to sanitize +file names can lead to path traversal vulnerabilities. Assuming a file’s MIME type is safe can lead to parsing exploits. + +## Recommendation: Use signed URLs -The server is responsible for parsing the multipart body, interpreting the `map`, and replacing -variable paths with the corresponding file streams. +The most secure and scalable approach is to **avoid uploading files through GraphQL entirely**. Instead: -## Implementing uploads with graphql-http +1. Use a GraphQL mutation to request a signed upload URL from your storage provider (e.g., Amazon S3). +2. Upload the file directly from the client using that URL. +3. Submit a second mutation to associate the uploaded file with your application’s data. -The `graphql-http` package doesn’t handle multipart requests out of the box. To support file -uploads, you’ll need to: +This approach isolates the file upload concern to infrastructure purpose-built for it, while keeping GraphQL focused on structured data. -1. Parse the multipart form request. -2. Map the uploaded file(s) to GraphQL variables. -3. Inject those into the request body before passing it to `createHandler()`. +## If you still choose to support uploads -Here's how to do it in an Express-based server using JavaScript and the [`busboy`](https://www.npmjs.com/package/busboy), -a popular library for parsing `multipart/form-data`. +If your application truly requires file uploads through GraphQL, proceed with caution. At a minimum, you should: -### Example: Express + graphql-http + busboy +- Use a well-maintained implementation of the +[GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec). +- Enforce a rule that upload variables are only referenced once. +- Always stream uploads to disk or cloud storage—never buffer them in memory. +- Apply strict request size limits and validate all fields. +- Treat file names, types, and contents as untrusted data. + +## Example (not recommended for production) + +The example below demonstrates how uploads could be wired up using Express, `graphql-http`, and busboy. +It’s included only to illustrate the mechanics and is not production-ready. + + + We strongly discourage using this code in production. + ```js import express from 'express'; @@ -110,74 +118,3 @@ app.post('/graphql', (req, res, next) => { app.listen(4000); ``` - -This example: - -- Parses `multipart/form-data` uploads. -- Extracts GraphQL query and variables from the `operations` field. -- Inserts file streams in place of `Upload` variables. -- Passes the modified request to `graphql-http`. - -## Defining the upload scalar - -The GraphQL schema must include a custom scalar type for uploaded files: - -```graphql -scalar Upload - -extend type Mutation { - uploadFile(file: Upload!): FileMetadata -} - -type FileMetadata { - filename: String! - mimetype: String! -} -``` - -In your resolvers, treat `file` as a readable stream: - -```js -export const resolvers = { - Upload: GraphQLScalarType, // implement as needed, or treat as opaque in resolver - Mutation: { - uploadFile: async (_, { file }) => { - const chunks = []; - for await (const chunk of file) { - chunks.push(chunk); - } - // process or store the file as needed - return { - filename: 'uploaded-file.txt', - mimetype: 'text/plain', - }; - } - } -}; -``` - -You can define `Upload` as a passthrough scalar if your server middleware already -handles file parsing: - -```js -import { GraphQLScalarType } from 'graphql'; - -export const Upload = new GraphQLScalarType({ - name: 'Upload', - serialize: () => { throw new Error('Upload serialization unsupported'); }, - parseValue: value => value, - parseLiteral: () => { throw new Error('Upload literals unsupported'); } -}); -``` - -## Best practices - -- Streaming: Don’t read entire files into memory. Instead, stream files to disk or an external -storage service. This reduces memory pressure and improves -scalability. -- Security: Always validate file types, restrict maximum file sizes, and sanitize filenames to prevent -path traversal or injection vulnerabilities. -- Alternatives: For large files or more scalable architectures, consider using pre-signed URLs -with an object storage service like S3. The client uploads the file directly, and the GraphQL -mutation receives the file URL instead. -- Client support: Use a client library that supports the GraphQL multipart request specification. From 12dd90609257763b8417379db596e4a700a574f6 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 19 Jun 2025 21:56:26 -0400 Subject: [PATCH 03/11] nits --- src/pages/learn/file-uploads.mdx | 36 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 68d631d62b..32f0bfa7ee 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -18,39 +18,49 @@ environments, these solutions often introduce complexity, fragility, and securit ### Memory exhaustion from repeated variables -GraphQL operations allow the same variable to be referenced multiple times. If a file upload variable is used more than -once, its stream may be consumed multiple times—or worse, not at all. This can lead to unpredictable behavior or denial of service (DoS). +GraphQL operations allow the same variable to be referenced multiple times. If a file upload variable is reused, the underlying +stream may be read multiple times or prematurely drained. This can result in incorrect behavior or memory exhaustion. + +A safe practice is to use trusted documents or a validation rule to ensure each upload variable is referenced exactly once. ### Stream leaks on failed operations -GraphQL executes in phases: validation, then execution. If an error occurs during validation or authorization, your -server might never reach the resolver that consumes a file stream. If file streams are left unconsumed, memory usage can -spike, potentially exhausting server resources. +GraphQL executes in phases: validation, then execution. If validation fails or an authorization check blocks execution, uploaded +file streams may never be consumed. If your server buffers or retains these streams, it can cause memory leaks. + +To avoid this, consider writing incoming files to temporary storage immediately, and passing references (like filenames) into +resolvers. Ensure this storage is cleaned up after request completion, regardless of success or failure. ### Cross-Site Request Forgery (CSRF) -`multipart/form-data` is classified as a “simple” request by CORS and does not trigger preflight checks. Without strict CSRF -protections, malicious sites may be able to upload files on behalf of unsuspecting users. +`multipart/form-data` is classified as a “simple” request in the CORS spec and does not trigger a preflight check. Without +explicit CSRF protection, your GraphQL server may unknowingly accept uploads from malicious origins. ### Oversized or excess payloads -Attackers can upload arbitrarily large files or extra files not referenced in the GraphQL operation. If your server accepts and -buffers these files in memory, you may face reliability issues or be vulnerable to resource exhaustion. +Attackers may submit very large uploads or include extraneous files under unused variable names. Servers that accept and +buffer these can be overwhelmed. + +Enforce request size caps and reject any files not explicitly referenced in the map field of the multipart payload. ### Untrusted file metadata -Uploaded file names, MIME types, and even contents are arbitrary and should be treated as untrusted input. Failing to sanitize -file names can lead to path traversal vulnerabilities. Assuming a file’s MIME type is safe can lead to parsing exploits. +Information such as file names, MIME types, and contents should never be trusted. To mitigate risk: + +- Sanitize filenames to prevent path traversal or injection issues. +- Sniff file types independently of declared MIME types, and reject mismatches. +- Validate file contents. Be aware of format-specific exploits like zip bombs or maliciously crafted PDFs. ## Recommendation: Use signed URLs -The most secure and scalable approach is to **avoid uploading files through GraphQL entirely**. Instead: +The most secure and scalable approach is to avoid uploading files through GraphQL entirely. Instead: 1. Use a GraphQL mutation to request a signed upload URL from your storage provider (e.g., Amazon S3). 2. Upload the file directly from the client using that URL. 3. Submit a second mutation to associate the uploaded file with your application’s data. -This approach isolates the file upload concern to infrastructure purpose-built for it, while keeping GraphQL focused on structured data. +This separates responsibilities cleanly, protects your server from binary data handling, and aligns with best practices for +modern web architecture. ## If you still choose to support uploads From 4ff70a9c9b57524a463e3c446526f10b3bbff6e7 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:20:18 -0400 Subject: [PATCH 04/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 32f0bfa7ee..04919a0ab2 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -7,7 +7,7 @@ This guide explains why file uploads via GraphQL are problematic and presents sa ## Why uploads are challenging -The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic and assumes requests are encoded as JSON. +The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic and serialization-agnostic (though HTTP and JSON are the most prevalent combination seen in the community). File uploads, by contrast, require `multipart/form-data` encoding to transfer binary data—something JSON can’t handle. Supporting uploads over GraphQL usually involves adopting community conventions, like the From 915873ea740d9f4e0252997b02edbe654c10ee04 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:09 -0400 Subject: [PATCH 05/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 04919a0ab2..15bc221d8f 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -8,7 +8,11 @@ This guide explains why file uploads via GraphQL are problematic and presents sa ## Why uploads are challenging The [GraphQL specification](https://spec.graphql.org/draft/) is transport-agnostic and serialization-agnostic (though HTTP and JSON are the most prevalent combination seen in the community). -File uploads, by contrast, require `multipart/form-data` encoding to transfer binary data—something JSON can’t handle. +GraphQL was designed to work with relatively small requests from clients, and was not designed with handling binary data in mind. + +File uploads, by contrast, typically handle binary data such as images and PDFs — something many encodings, including JSON, cannot handle directly. +One option is to encode within our encoding (e.g. use a base64-encoded string within our JSON), but this is inefficient and is not suitable for larger binary files as it does not support streamed processing easily. +Instead, `multipart/form-data` is a common choice for transferring binary data; but it is not without its own set of complexities. Supporting uploads over GraphQL usually involves adopting community conventions, like the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). While useful in some From ee805e27c428af9336435a5f77ae9f5ac58ee02e Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:16 -0400 Subject: [PATCH 06/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 15bc221d8f..0b4eb94b88 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -14,9 +14,11 @@ File uploads, by contrast, typically handle binary data such as images and PDFs One option is to encode within our encoding (e.g. use a base64-encoded string within our JSON), but this is inefficient and is not suitable for larger binary files as it does not support streamed processing easily. Instead, `multipart/form-data` is a common choice for transferring binary data; but it is not without its own set of complexities. -Supporting uploads over GraphQL usually involves adopting community conventions, like the -[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). While useful in some -environments, these solutions often introduce complexity, fragility, and security risks. +Supporting uploads over GraphQL usually involves adopting community conventions, the most prevalent of which is the +[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). +This specification has been successfully implemented in many languages and frameworks, but users +implementing it must pay very close attention to ensure that they do not introduce +security or reliability concerns. ## Risks to be aware of From fcbfc58ead524b79f1ecfee837daaa9cb325d446 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:27 -0400 Subject: [PATCH 07/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 0b4eb94b88..7f6bdbde2c 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -75,7 +75,8 @@ If your application truly requires file uploads through GraphQL, proceed with ca - Use a well-maintained implementation of the [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec). - Enforce a rule that upload variables are only referenced once. -- Always stream uploads to disk or cloud storage—never buffer them in memory. +- Stream uploads to disk or cloud storage—avoid buffering them in memory. +- Ensure that streams are always terminated when the request ends, whether or not they were consumed. - Apply strict request size limits and validate all fields. - Treat file names, types, and contents as untrusted data. From 39768a0804871a55d264e895265e86e4a1b48db7 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:34 -0400 Subject: [PATCH 08/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 7f6bdbde2c..f26b3c5012 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -63,7 +63,10 @@ The most secure and scalable approach is to avoid uploading files through GraphQ 1. Use a GraphQL mutation to request a signed upload URL from your storage provider (e.g., Amazon S3). 2. Upload the file directly from the client using that URL. -3. Submit a second mutation to associate the uploaded file with your application’s data. +3. Submit a second mutation to associate the uploaded file with your application’s data (or use an automatically triggered process, such as Amazon Lambda, to do the same). + +You should ensure that these file uploads are only retained for a short period such that an attacker completing only steps 1 and 2 will not exhaust your storage. +When processing the file upload (step 3), the file should be moved to more permanent storage as appropriate. This separates responsibilities cleanly, protects your server from binary data handling, and aligns with best practices for modern web architecture. From 3a26d6020a2fa3de620f09170290e2988727749e Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:40 -0400 Subject: [PATCH 09/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index f26b3c5012..2bbbfacc9d 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -34,7 +34,8 @@ A safe practice is to use trusted documents or a validation rule to ensure each GraphQL executes in phases: validation, then execution. If validation fails or an authorization check blocks execution, uploaded file streams may never be consumed. If your server buffers or retains these streams, it can cause memory leaks. -To avoid this, consider writing incoming files to temporary storage immediately, and passing references (like filenames) into +To avoid this, ensure that all streams are terminated when the request finishes, whether or not they were consumed in resolvers. +An alternative to consider is writing incoming files to temporary storage immediately, and passing references (like filenames) into resolvers. Ensure this storage is cleaned up after request completion, regardless of success or failure. ### Cross-Site Request Forgery (CSRF) From 9663c7d77080b8b1ed54c15bded3f8acd5f15d3b Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:49 -0400 Subject: [PATCH 10/11] Update src/pages/learn/file-uploads.mdx Co-authored-by: Benjie --- src/pages/learn/file-uploads.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 2bbbfacc9d..13e8cb492a 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -31,7 +31,7 @@ A safe practice is to use trusted documents or a validation rule to ensure each ### Stream leaks on failed operations -GraphQL executes in phases: validation, then execution. If validation fails or an authorization check blocks execution, uploaded +GraphQL executes in phases: validation, then execution. If validation fails or an authorization check prematurely terminates execution, uploaded file streams may never be consumed. If your server buffers or retains these streams, it can cause memory leaks. To avoid this, ensure that all streams are terminated when the request finishes, whether or not they were consumed in resolvers. From 3060d1624b3312c92cfd6d25bfa1ec353039b982 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Fri, 20 Jun 2025 07:27:05 -0400 Subject: [PATCH 11/11] remove example --- src/pages/learn/file-uploads.mdx | 56 -------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/pages/learn/file-uploads.mdx b/src/pages/learn/file-uploads.mdx index 13e8cb492a..16afe53f22 100644 --- a/src/pages/learn/file-uploads.mdx +++ b/src/pages/learn/file-uploads.mdx @@ -83,59 +83,3 @@ If your application truly requires file uploads through GraphQL, proceed with ca - Ensure that streams are always terminated when the request ends, whether or not they were consumed. - Apply strict request size limits and validate all fields. - Treat file names, types, and contents as untrusted data. - -## Example (not recommended for production) - -The example below demonstrates how uploads could be wired up using Express, `graphql-http`, and busboy. -It’s included only to illustrate the mechanics and is not production-ready. - - - We strongly discourage using this code in production. - - -```js -import express from 'express'; -import busboy from 'busboy'; -import { createHandler } from 'graphql-http/lib/use/express'; -import { schema } from './schema.js'; - -const app = express(); - -app.post('/graphql', (req, res, next) => { - const contentType = req.headers['content-type'] || ''; - - if (contentType.startsWith('multipart/form-data')) { - const bb = busboy({ headers: req.headers }); - let operations, map; - const files = {}; - - bb.on('field', (name, val) => { - if (name === 'operations') operations = JSON.parse(val); - else if (name === 'map') map = JSON.parse(val); - }); - - bb.on('file', (fieldname, file, { filename, mimeType }) => { - files[fieldname] = { file, filename, mimeType }; - }); - - bb.on('close', () => { - for (const [key, paths] of Object.entries(map)) { - for (const path of paths) { - const keys = path.split('.'); - let target = operations; - while (keys.length > 1) target = target[keys.shift()]; - target[keys[0]] = files[key].file; - } - } - req.body = operations; - next(); - }); - - req.pipe(bb); - } else { - next(); - } -}, createHandler({ schema })); - -app.listen(4000); -```