Apollo 4 GET Sandbox causing CSRF on server

I am working through an upgrade from Apollo 3 to 4 using Express. I’ve added body-parser and cors as laid out in the upgrade instructions and the endpoint is working pretty much as expected.

What I am seeing is when a GET request is made to open the Apollo Sandbox the following error is thrown on the server:

“This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a ‘content-type’ header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight\n”

cors settings are '*' and I am hoping someone could shed some light on why I am seeing this error. Also, to clarify, the Apollo Sandbox site does work even though that error is thrown on the server.

It appears that the GET request is going against the GraphQL endpoint before loading the Sandbox, the error makes sense in that regard, though I have no idea how to keep it from happening.

Thank you.

You specifically mean loading the Sandbox that is a landing page on your server, not by navigating to the Sandbox URL on Apollo Studio, right?

How are you running your server? Is it possible it’s behind some sort of layer that does not pass the accept header through to your server, and so your server doesn’t know it is being loaded from a browser and does not display the landing page? Although perhaps I’m not sure I follow what you mean by the site actually working. Is this hosted somewhere where we could see it?

Correct

Currently I am experiencing this on localhost.

By the is actually working, I mean, even though the server is displaying the CSRF error, the sandbox still loads and I am able to use it. It’s just each time it is requested that error is received. I am guessing that Apollo Server is returning that error prior to the request making it to the Sandbox.

That’s surprising. It appears to be the main request that is loading the HTML for Sandbox? Or something like the introspection request that it later sends to your server?

(Also, the error in question is returned to the user over HTTP and I don’t believe it is typically printed in your server by default, unless you have some extra logging turned on.)

I honestly don’t know

So this is showing on the server as the formatError function created when setting ApolloServer has a debug log statement writing the error out to the console (stdout).

  gce:api:graphqlServer from formatError {
  gce:api:graphqlServer   message: "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight\n",
  gce:api:graphqlServer   extensions: {
  gce:api:graphqlServer     code: 'BAD_REQUEST',
  gce:api:graphqlServer     stacktrace: [
  gce:api:graphqlServer       "BadRequestError: This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight",
  gce:api:graphqlServer       '',
  gce:api:graphqlServer       '    at new GraphQLErrorWithCode (/Users/brettski/drifting/api/node_modules/@apollo/server/dist/cjs/internalErrorClasses.js:10:9)',
  gce:api:graphqlServer       '    at new BadRequestError (/Users/brettski/drifting/api/node_modules/@apollo/server/dist/cjs/internalErrorClasses.js:84:9)',
  gce:api:graphqlServer       '    at preventCsrf (/Users/brettski/drifting/api/node_modules/@apollo/server/dist/cjs/preventCsrf.js:35:11)',
  gce:api:graphqlServer       '    at ApolloServer.executeHTTPGraphQLRequest (/Users/brettski/drifting/api/node_modules/@apollo/server/dist/cjs/ApolloServer.js:507:50)',
  gce:api:graphqlServer       '    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)'
  gce:api:graphqlServer     ]
  gce:api:graphqlServer   }
  gce:api:graphqlServer } +3m

Can you look in your browser’s dev console Network tab to see exactly what request is the one that’s being blocked?

Oh this is a bit funny. In the network tab the only localhost reference being blocked is favicon.ico, it has a 400 response with a CSRF response message.

To experiment I turned of csrfPrevention and saw the following:

in the server console a different error is shown:

GraphQL operations must contain a non-empty query or a persistedQuery extension.

and in the browser, the same favicon.ico resulted in a 400 response (again the only file), though there was no other response or message associated with the request.

Well, that does make sense. The favicon fetch won’t send accept: text/html so it isn’t treated as a landing page request. But it also isn’t a valid GraphQL operation.

If you don’t want to serve a favicon from your server, then you certainly are likely to get fetches at that URL from normal users, so logging those errors as if they are serious is probably a mistake. I suppose we could make Apollo Server return a different 404 error if the middleware sees an accept: image/* request, though that seems a bit problematic (eg, in Chrome this header also contains */* which would match application/json).

Interesting. For clarity, a 404 is not returned currently it is a 400 Bad Request error in both cases (with or without CSRF setting on/off). Which as I think about this I could probably handle in express before the request makes it to Apollo :thinking:

Maybe try adding a landing page plugin and see what happens
Something like this

import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    // Install a landing page plugin based on NODE_ENV
    process.env.NODE_ENV === 'production'
      ? ApolloServerPluginLandingPageProductionDefault()
      : ApolloServerPluginLandingPageLocalDefault(),
  ],
});