# CSRF Prevention Mercurius includes built-in Cross-Site Request Forgery (CSRF) prevention to protect your GraphQL endpoints from malicious requests. ## What is CSRF? [Cross-Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf) attacks exploit the fact that browsers automatically include cookies and other credentials when making requests to websites. An attacker can create a malicious website that makes requests to your GraphQL server using the victim's credentials. CSRF attacks are particularly dangerous for "simple" requests that don't trigger a CORS preflight check. These attacks can: - Execute mutations using an authenticated user's credentials - Extract timing information from queries (XS-Search attacks) - Abuse any GraphQL operations that have side effects ## How CSRF Prevention Works Mercurius protects against CSRF attacks by ensuring that GraphQL requests do **not** qualify as “simple” requests under the [CORS specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests). A request is considered safe if **any** of the following conditions are met: ### 1. Content-Type Header Requests that include a `Content-Type` header specifying a type **other than**: * `text/plain` * `application/x-www-form-urlencoded` * `multipart/form-data` will trigger a **preflight `OPTIONS` request**, meaning the request cannot be considered “simple.” By default, Mercurius allows the following `Content-Type` headers: * `application/json` (recommended and most common) * `application/graphql` Note charset and other params are ignored ### 2. Required Headers Requests that include a **custom header** also require a **preflight `OPTIONS` request**, preventing them from being “simple.” By default, Mercurius checks for one of the following headers: * `X-Mercurius-Operation-Name` * `Mercurius-Require-Preflight` ## Configuration ### Enabling CSRF Prevention CSRF prevention is **disabled by default**. Enable it with: ```javascript const app = Fastify() await app.register(mercurius, { schema, resolvers, csrfPrevention: true // Enable with default settings }) ``` Default required headers (case insensitive): - `x-mercurius-operation-name` - Custom header for identifying GraphQL operations - `mercurius-require-preflight` - General-purpose header for forcing preflight ### CORS Configuration While not strictly necessary, CORS should be configured appropriately: ```javascript await app.register(require('@fastify/cors'), { origin: ['https://your-frontend.com'] }) await app.register(mercurius, { schema, resolvers, csrfPrevention: true }) ``` ## Advanced Configuration ### Custom Required Headers Configure which headers are accepted to bypass CSRF protection (these replace the default headers): ```javascript await app.register(mercurius, { schema, resolvers, csrfPrevention: { contentTypes: ['application/json', 'application/graphql', 'application/vnd.api+json'], requiredHeaders: ['Authorization', 'X-Custom-Header', 'X-Another-Header'] } }) ``` ### Disabling CSRF Prevention ```javascript await app.register(mercurius, { schema, resolvers, csrfPrevention: false }) ``` ### Enabling File Upload File uploads require a `multipart/form-data` request. To enable CSRF protection for file uploads, the request must include both: * `Content-Type: multipart/form-data` * A custom header ```javascript import mercuriusUpload from 'mercurius-upload'; import mercurius from 'mercurius'; await app.register(mercuriusUpload); await app.register(mercurius, { schema, resolvers, csrfPrevention: { contentTypes: ['application/json', 'multipart/form-data'], requiredHeaders: ['X-Custom-Header'] } }); ``` This configuration ensures that file uploads trigger a preflight `OPTIONS` request, preventing them from being treated as "simple" requests and keeping your API safe from CSRF attacks. ## Client Integration For custom GraphQL clients, ensure your requests include one of the following: ### Option 1: Use application/json content-type (recommended) ```javascript fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '{ hello }' }) }) ``` ### Option 2: Include a required header ```javascript fetch('/graphql?query={hello}', { method: 'GET', headers: { 'mercurius-require-preflight': 'true' } }) ``` ## Complete Examples ### Basic Server Setup ```javascript const Fastify = require('fastify') const mercurius = require('mercurius') const app = Fastify({ logger: true }) const schema = ` type Query { hello: String users: [User] } type Mutation { createUser(name: String!): User } type User { id: ID! name: String! } ` const resolvers = { Query: { hello: () => 'Hello World', users: () => [{ id: '1', name: 'John' }] }, Mutation: { createUser: (_, { name }) => ({ id: Date.now().toString(), name }) } } // Register CORS (recommended) await app.register(require('@fastify/cors'), { origin: ['https://your-frontend.com'], credentials: true }) // Register Mercurius with CSRF protection await app.register(mercurius, { schema, resolvers, csrfPrevention: true, // Enable CSRF protection }) await app.listen({ port: 4000, host: '0.0.0.0' }) console.log('GraphQL server running on http://localhost:4000/graphql') ``` ### Frontend Client Example ```javascript // React/Frontend example with proper headers const client = { query: async (query, variables = {}) => { const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', // Optional: Add custom identification 'x-mercurius-operation-name': 'ClientQuery' }, body: JSON.stringify({ query, variables }) }) if (!response.ok) { throw new Error(`GraphQL Error: ${response.status}`) } return response.json() } } // Usage try { const result = await client.query('{ hello }') console.log(result.data.hello) } catch (error) { console.error('CSRF or other error:', error) } ``` ## Testing CSRF Prevention ### Testing Blocked Requests ```javascript // This request will be blocked (400 status) const response = await fetch('/graphql?query={hello}', { method: 'GET' // No required headers or valid content-type }) console.log(response.status) // 400 ``` ### Testing Allowed Requests ```javascript // This request will succeed const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '{ hello }' }) }) console.log(response.status) // 200 ``` ## Error Response When a request is blocked by CSRF prevention, you'll receive a 400 status with the following error: ```json { "data": null, "errors": [{ "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF)." }] } ``` ## Migration Guide If you're adding CSRF prevention to an existing Mercurius application: ### For Most Applications ✅ **No action required** - Most GraphQL clients already send appropriate headers. ### If You See CSRF Errors 1. **Check your client** - Ensure it sends `Content-Type: application/json` for POST requests 2. **Add required headers** - For GET requests, add `mercurius-require-preflight: true` 3. **Configure custom headers** - If needed, add your client's headers to `requiredHeaders` ### Legacy Client Support For clients that can't be easily updated: ```javascript await app.register(mercurius, { schema, resolvers, csrfPrevention: { requiredHeaders: [ 'x-mercurius-operation-name', 'mercurius-require-preflight', 'User-Agent', // Many clients send this automatically 'X-Requested-With' // Common in AJAX libraries ] } }) ``` ## Security Considerations ### When CSRF Prevention is Critical - Applications with authentication/authorization - APIs that perform mutations or have side effects - Public-facing GraphQL endpoints - Applications handling sensitive data ### When CSRF Prevention May Be Less Critical - Public read-only APIs with no authentication - Internal APIs on isolated networks - Development environments (consider disabling temporarily) ### Best Practices 1. **Keep CSRF prevention enabled** in production 2. **Use HTTPS** to prevent header manipulation 3. **Implement proper CORS policies** as an additional layer 4. **Monitor for blocked requests** to catch client issues 5. **Test thoroughly** when adding custom required headers ## Troubleshooting ### Common Issues **Q: My requests are being blocked with a 400 error** A: Ensure your client sends `Content-Type: application/json` or add `mercurius-require-preflight: true` header. **Q: GraphiQL stopped working** A: GraphiQL should work automatically. If not, check if you've misconfigured the routes or added overly restrictive headers. **Q: My frontend or mobile app requests are blocked** A: Check the HTTP client configuration. Most modern clients work automatically, but ensure proper Content-Type headers. **Q: I need to support a legacy client** A: Add the client's existing headers to `requiredHeaders`, or as a last resort, disable CSRF prevention. ### Debug Mode To debug CSRF prevention issues, you can temporarily log requests: ```javascript app.addHook('preHandler', async (request, reply) => { if (request.url.includes('/graphql')) { console.log('GraphQL request headers:', request.headers) console.log('Content-Type:', request.headers['content-type']) } }) ``` ### Testing Your Configuration Create a simple test to verify CSRF protection is working: ```javascript // test-csrf.js const test = async () => { // This should be blocked try { const blocked = await fetch('http://localhost:4000/graphql?query={hello}') console.log('CSRF test failed - request was not blocked:', blocked.status) } catch (error) { console.log('CSRF correctly blocked the request') } // This should work try { const allowed = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '{ hello }' }) }) console.log('Valid request succeeded:', allowed.status === 200) } catch (error) { console.log('Valid request failed:', error.message) } } test()