-
Notifications
You must be signed in to change notification settings - Fork 1.5k
docs: add guide on file uploads #2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sarahxsanders
wants to merge
11
commits into
graphql:source
Choose a base branch
from
sarahxsanders:file-uploads
base: source
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
928bd9c
add guide on file uploads
sarahxsanders c61bfc0
rewrite based on feedback
sarahxsanders 12dd906
nits
sarahxsanders 4ff70a9
Update src/pages/learn/file-uploads.mdx
sarahxsanders 915873e
Update src/pages/learn/file-uploads.mdx
sarahxsanders ee805e2
Update src/pages/learn/file-uploads.mdx
sarahxsanders fcbfc58
Update src/pages/learn/file-uploads.mdx
sarahxsanders 39768a0
Update src/pages/learn/file-uploads.mdx
sarahxsanders 3a26d60
Update src/pages/learn/file-uploads.mdx
sarahxsanders 9663c7d
Update src/pages/learn/file-uploads.mdx
sarahxsanders 3060d16
remove example
sarahxsanders File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Handling File Uploads in GraphQL | ||
|
||
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. | ||
|
||
This guide explains why file uploads via GraphQL are problematic and presents safer alternatives. | ||
|
||
## Why uploads are challenging | ||
|
||
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. | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Risks to be aware of | ||
|
||
### Memory exhaustion from repeated variables | ||
|
||
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 validation fails or an authorization check blocks execution, uploaded | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 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 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 | ||
|
||
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: | ||
|
||
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. | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
|
||
If your application truly requires file uploads through GraphQL, proceed with caution. At a minimum, you should: | ||
|
||
- 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. | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- 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. | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<Callout type="warning" emoji="⚠️"> | ||
We strongly discourage using this code in production. | ||
</Callout> | ||
|
||
```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); | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.