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) 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.
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/plainapplication/x-www-form-urlencodedmultipart/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-NameMercurius-Require-Preflight
Configuration
Enabling CSRF Prevention
CSRF prevention is disabled by default. Enable it with:
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 operationsmercurius-require-preflight- General-purpose header for forcing preflight
CORS Configuration
While not strictly necessary, CORS should be configured appropriately:
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):
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
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
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)
fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: '{ hello }' })
})Option 2: Include a required header
fetch('/graphql?query={hello}', {
method: 'GET',
headers: {
'mercurius-require-preflight': 'true'
}
})Complete Examples
Basic Server Setup
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
// 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
// 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) // 400Testing Allowed Requests
// 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) // 200Error Response
When a request is blocked by CSRF prevention, you’ll receive a 400 status with the following error:
{
"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
- Check your client - Ensure it sends
Content-Type: application/jsonfor POST requests - Add required headers - For GET requests, add
mercurius-require-preflight: true - Configure custom headers - If needed, add your client’s headers to
requiredHeaders
Legacy Client Support
For clients that can’t be easily updated:
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
- Keep CSRF prevention enabled in production
- Use HTTPS to prevent header manipulation
- Implement proper CORS policies as an additional layer
- Monitor for blocked requests to catch client issues
- 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:
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:
// 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()