HTTP 403 on OPTIONS request to origin server

I have a problem with an OPTIONS methods request to an origin server that is protected by a Cloud Flare access policy.

The curl for the request is equivalent to:

curl 'https://foo.acme.io/api/engine/engine/default/user' \
  -X 'OPTIONS' \
  -H 'authority: foo.acme.io' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36' \
  -H 'x-xsrf-token: {elided}' \
  -H 'origin: https://foo.acme.io' \
  -H 'sec-fetch-site: same-origin' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: https://foo.acme.io/app/admin/default/' \
  -H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'cookie: JSESSIONID={elided|; XSRF-TOKEN={elided}; __cfduid={elided}; _hp2_id.2360180481={elided}; {elided}={elided}; CF_Authorization={elided}' \
  --compressed

Various tokens have been elided for security reasons but the token names have been preserved to indicate that they are present.

The following HTTP response is generated:

HTTP/2 403 
date: Mon, 14 Dec 2020 00:59:44 GMT
cf-request-id: 07005bc11e0000fd4ab493b000000001
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=dH2wNpsXeCRGo53%2Bqj57tXiCMWhBGJE6bH6sBtxJFSgKA0WxCvO75YmYbrD4mqsNyAQjIpcH9p5xLbq4aATjHCoNyVJ19S36Yt0VZ8jy2z6z"}],"group":"cf-nel","max_age":604800}
nel: {"report_to":"cf-nel","max_age":604800}
vary: Accept-Encoding
strict-transport-security: max-age=15552000
server: cloudflare
cf-ray: 6013fbe1c941fd4a-SYD

If I replace the OPTIONS method by GET the request succeeds, as expected, indicating that the cookies themselves are valid. There is no trace of the request in the origin server and this is not a cross-origin request. If I add an ALLOW or BYPASS rule to the firewall for requests using the OPTIONS method I see the requests pass through the firewall, but they are still rejected (somewhere, in CloudFlare, with a 403).

All the CORS options in the access policy are at their defaults (e.g. unconfigured).

If this was a cross-origin request, I would understand that may be some additional CORS configuration would be required, but this is not the case in this particular case.

I have read all the troubleshooting material here - https://developers.cloudflare.com/access/faq and searched the forums but I haven’t found anything that explains why an OPTIONS request is generating a 403 in this case.

Can someone explain what it is about CloudFlare’s handling of OPTIONS method requests I am not understanding and/or how I diagnose what is going wrong and/or fix it?

Do you have Browser Integrity Check disabled in your Firewall security settings?

I did not have it disabled.

I tried disabling it and retested but the same behaviour resulted. I have now renabled Browser Integrity Check,

One other thing, since writing the above post to workaround the issue I added a bypass rule that allows the access policy to be bypassed from addresses on our VPN. When I did this OPTIONS requests did succeed (from within the VPN) as might be expected. The OPTIONS method (only) continues to be blocked when accessed from outside the VPN.

Any solutions for this? I’m experiencing a similar issue.

Thanks

Not as yet

I faced a similar issue when using WebSockets in Cloudflare Access, despite there being a bypass in place.

I’m not sure if this is the case with Cloudflare Access, but, passing web sockets normally require some additional config in the nginx proxying server. So, it might be something that Cloudflare could do on their Cloudflare Workers Script for Cloudflare Access?

More info:

Not sure if the WebSocket issue that I had faced and this OPTIONS method issue that you’re facing are the same/even related, but, in case the above short read from nginx helps.

I added the following worker that adds a “foo” response header that allows me to see it is executing and returns a 500 response in the case it sees an OPTIONS header being passed through.

I discovered that it never sees a request with an OPTIONS header (I am always seeing a 403 for requests with OPTIONS headers) indicating that the request is being rejected with a 403 before reaching the workers. I tested the worker in the GUI and the behaviour is different - it does return a 500 response in this case, indicating that the logic in the worker itself is behaving as expected.

I also temporarily disabled the WAF but this didn’t change the problematic behaviour either.

/**
 * @param {string} headerNameSrc Header to get the new value from
 * @param {string} headerNameDst Header to set based off of value in src
 */
const headerNameSrc = "X-foo" //"Orig-Header"

async function handleRequest(request) {
  /**
   * Response properties are immutable. To change them, construct a new
   * Response and pass modified status or statusText in the ResponseInit
   * object. Response headers can be modified through the headers `set` method.
   */
  if (request.method == 'OPTIONS') {
    return new Response(null, {status: 500, statusText: 'Detected OPTIONS'})
  }


  const originalResponse = await fetch(request)

  // Change status and statusText, but preserve body and headers
  let response = new Response(originalResponse.body, {
    status: originalResponse.status,
    statusText:  originalResponse.statusText,
    headers: { ...originalResponse.headers, "foo": "bar"}
  })

  return response
}

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request))
})

I have also found that the issue only occurs if a Cloudflare Access BYPASS rule is not in effect.

If Cloudflare Access is processing requests using an ALLOW rule (based on membership of an access group) then the 403 issue occurs before the point where the worker is invoked.

If Cloudflare Access has used a IP based BYPASS rule to process the request, then the 403 issue does not occur and a worker that checks for the presence of the OPTIONS method works as expected.