Addressing CSRF from a Python service

I have a Python microservice that is attempting to hit an Apollo Server 4 backend. However, I’m running into issues where I keep getting the “CSRF” error. I’m struggling on how to actually address this issue based on what I know from the documentation.

  • specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain)

I tried this but I’m still running into the error.

  • or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight\n

I believe this only to be used by the Apollo iOS/Android clients so I’m not sure if this will actually work for me. What do I need to do in Python to actually make the apollo-require-preflight header work? Do I only need to add this to the header, or do I need to make another request?

I’m currently just using requests in Python but would be happy to migrate to another module/library if one solves this use case better.

Are you sending a GET request or a POST request? If a POST request, does your request not contain content-type: application/json? I’d expect most POST requests to work.

If you’re using a GET request you can just send any non-empty apollo-require-preflight header. The point of CSRF prevention is to avoid problems specific to web-browser-based clients, by only processing requests that would put the browser request logic into a “preflight” mode. CSRF prevention is not relevant for non-web-browser clients, which is why you can get around it by what may seem to be an incredibly trivial step (adding a apollo-require-preflight: true header).

Sending this header doesn’t require Apollo Server or your Python client to perform a preflight: but if you were in a web browser, the very act of adding a custom header like apollo-require-preflight would make your browser perform a preflight request.

Are you sending a GET request or a POST request?

POST right now, but I could change this to send a GET request.

If a POST request, does your request not contain content-type: application/json ?

Yes, this is an example request:

headers = {'cookie': 'token=Bearer %s' % GraphQLAccessor.last_token, 'Content-Type': 'application/json', 'Apollo-Require-Preflight': 'true'}
payload = {"query": self.query_info}
requests.post(self.gql_endpoint, json=payload, headers=headers).json()

content-type: application/json should be sufficient to avoid this error. Something strange is happening here, and perhaps it’s in your Python code? Or maybe you’re not talking to the server you think you’re talking to?

Looks like you can get more logging out of requests via some urllib3 logging: Developer Interface — Requests 2.28.2 documentation

The Python error is pretty specific which is why I know it’s a CSRF issue. Here’s the full error I get back from Python:

Login response:b’{“errors”:[{“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”,“extensions”:{“code”:“BAD_REQUEST”,“stacktrace”:[“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”,“”,"

It was my understanding that if I included 'content-type':'application/json' as a header in my Python request, I would need to have a preflight check, based on this documentation:

The most important rule for whether or not a request is “simple” is whether it tries to set arbitrary HTTP request headers. Any request that sets the Content-Type header to application/json (or anything other than a list of three particular values) cannot be a simple request, and thus it must be preflighted. Because all POST requests recognized by Apollo Server must contain a Content-Type header specifying application/json, we can be confident that they are not simple requests and that if they come from a browser, they have been preflighted.

I know I’m hitting the right server because I get this error log from both my microservice logging, and my GraphQL backend records an error at the exact same time, so my microservice is hitting the service I expect it to.

I am pretty sure that you are not successfully transmitting content-type: application/json to your ApolloServer. Either requests isn’t sending what you think it is, or your Apollo Server is set up weirdly (are you using startStandaloneServer? expressMiddleware?).

It was my understanding that if I included 'content-type':'application/json' as a header in my Python request, I would need to have a preflight check, based on this documentation:

In the docs you’re quoting, “it must be preflighted” should really say “and thus if it came from a browser, the browser must have chosen to preflight it”. If you’re not a browser then you don’t have to preflight (but you do need to make your request look like “a request that, if it had been in a browser, would have been preflighted”).

I’m using expressMiddleware, and I also added a CORS exception for my microservice when I create Apollo Server in my application backend. That might be where I’m stumbling - do I need to configure CORS to explicitly allow other requests to pass content-type and other headers? This works fine in my application frontend, so I’m not so sure that’s what’s up.

Here’s how I have the server set up:

  const server = new ApolloServer<Context>({
    cache: "bounded",
    csrfPrevention: true,
    includeStacktraceInErrorResponses: true,
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
    schema
  });

  // Start the GraphQL server
  await server.start();

app.use(
    "/graphql",
    cors<cors.CorsRequest>({
      credentials: true,
      methods: ["GET", "OPTIONS", "POST"],
      origin: [process.env.FRONTEND_URL, process.env.SERVICE_URL]
    }),
    json(),
    expressMiddleware(server, {
      context: async ({ req, res }): Promise<Context> => {
       // context
      }
    })
  );

Consider adding a middleware between json and expressMiddleware that prints out incoming headers.

Looks like the content-type header is definitely being received by the server. I see it in the logged header as "content-type":"application/json".

This is what I added:

    json(),
    (req, res, next) => {
      console.log(JSON.stringify(req.headers));
      return next();
    },
    expressMiddleware(server, {

And it output something like this (some headers removed for security):

2023-02-14T15:07:26.709349660Z {"accept":"*/*","accept-encoding":"gzip, deflate","max-forwards":"10","user-agent":"python-requests/2.28.1","x-original-url":"/graphql","x-waws-unencoded-url":"/graphql","content-type":"application/json","content-length":"1104"}

This is quite strange. If you’re able to put this together as a reproduction (a git repo or codesandbox.io sandbox or something with clear instructions about how to see it) and post a link here or as a new issue on the apollo-server repo, we’d be happy to look into it further.