StackHawk
Hamburger Icon

Fixing "No
'Access-Control-Allow-Origin'
Header Present"

tim-armstrong-headshot

Tim Armstrong|June 18, 2021

"No 'access-control-allow-origin' header present" is up there as one of the least helpful error messages. Searching for it on the internet is likely to bring up a Stack-Overflow answer that is worse than wrong – it's dangerous. So, what is it and why is it breaking your web app?

How to Fix "No 'Access-Control-Allow-Origin' Header Present"

This error is up there as one of the least helpful error messages. Sure, it tells you that there's a header missing, but from where is it missing, and what should it be? Searching for it on the internet is likely to bring up a popular forum where the most common answer is worse than wrong – it's dangerous.

In short, the 'access-control-allow-origin' header is a Cross-Origin Resource Sharing (CORS) header. We've already written an explainer on what CORS headers are and what they do (which you can find here), but to summarize: CORS is a mechanism for relaxing the "Same-Origin" policy of modern browsers to allow things like serving your static content from www.example.com and your dynamic content from api.example.com.

So, What is This Error Then?

This error occurs when a script on your website/web app attempts to make a request to a resource that isn't configured to accept requests coming from code that doesn't come from the same (sub)domain, thus violating the Same-Origin policy.

Let's take a look at what's actually going on under the hood of the browser when this occurs.

Fixing "No 'Access-Control-Allow-Origin' Header Present" image

As you can see from the sequence diagram, before making the script's actual request to the requested resource, the browser first makes a preflight request for the resource's OPTIONS. This allows the resource to define the policy that the browser should enforce on all scripts that wish to contact it. If the constraints set by the resource are met by the script's request then the browser's access control check will pass, allowing the actual request to proceed.

If the requested resource isn't configured to answer the OPTIONS request method or isn't configured to handle it correctly, then you'll see this error.

There Are Two Approaches to Getting It Right.

  1. Use a reverse proxy server or WSGI server(such as Nginx or Apache) to proxy requests to your resource and handle the OPTIONS method in the proxy.

  2. Add support for handling the OPTIONS method in the resource's code.

Both of these methods are equally valid but have different use-cases.

The first method is ideal if what you're looking for is a quick fix to supporting a small number of sources of Cross-Origin requests, but gets very complex if you need the flexibility of supporting multiple sources with varying constraints. The second method provides much more flexibility and the ability to change the response dynamically, but at the expense of greater initial complexity. So, which solution you go with really comes down to your needs.

Basic Implementation Concepts

In order to implement the bare minimum support for Cross-Origin Resource Sharing, we need to understand what the browser is trying to validate and why. We have a more detailed explainer on that here, but to summarize: The goal of the browser is to ensure that it's not leaking user data to potentially malicious scripts. In that mindset, browsers assume that any scripts that come from (sub)domains other than the resource they're trying to contact should not be able to contact that resource or make use of any of the credentials to identify the user to that resource. The `OPTIONS` request is how the browser asks the resource if scripts from a given origin are safe, and what they should be allowed to do.

Therefore the bare minimum we need is being able to handle the `OPTIONS` method and perform some validation on the headers received there. The minimum validation we need to support here is ensuring that the `Origin` header to ensure matches an expected value (access lists and regex rules are common solutions here). If the validation passes, then we want to reply with our CORS headers providing confirmation to the browser that:

  • The `Origin` is trusted

  • What `Method`s scripts from that origin are trusted to use

  • If the browser should allow the script to make authenticated requests (carrying credentials such as `Authorization` headers or Cookies)

  • Any request headers that should be allowed with the request

  • Any response headers to expect and pass on to the script

  • And how long to cache this response for

While setting some of these response headers to a wildcard is technically possible, doing so will almost always lead to unexpected data leak risks, and so should only be done for public/unauthenticated resources (such as a time service, or public stream of some sort).

Implementing CORS with a Reverse-Proxy

So using Nginx as an example, let's take a look at how to fix the "No 'access-control-allow-origin' header present" error.

A quick search on any popular search engine will reveal a number of solutions that look super simple, but all essentially boil down to disabling the browser's security policy for all requests to that domain (not something we'd recommend). So how do we set up our reverse proxy securely?

Let's take a look at the finished config template, and then break down what each directive is doing.

server {
    listen ${NGINX_PORT};
    listen [::]:${NGINX_PORT};
    server_name ${NGINX_HOST};
    location / {
        if ($http_origin ~* "^http://www.example.com:8080$") {
            add_header Access-Control-Allow-Origin "$http_origin";
            add_header Access-Control-Allow-Methods "OPTIONS, POST, GET";
            add_header Access-Control-Max-Age "3600";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Allow-Headers "Content-Type";
            set $test  "A";
        }
        if ($request_method = 'OPTIONS') {
            set $test  "${test}B";
        }
        if ($test = "AB") {
            add_header Access-Control-Allow-Origin "$http_origin";
            add_header Access-Control-Allow-Methods "OPTIONS, DELETE, POST, GET, PATCH, PUT";
            add_header Access-Control-Max-Age "3600";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Allow-Headers "Content-Type";
            return 204;
        }
        if ($test = "B") {
            return 403;
        }
        proxy_pass "${PROXY_HOST}:${PROXY_PORT}";
        proxy_http_version  1.1;
        proxy_cache_bypass  $http_upgrade;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
    }
}

In general, when dealing with Nginx the common rule is that "if is evil." So what are we doing putting 4 of them in? Well, to quote the same post: "There are cases where you simply cannot avoid using an if, for example, if you need to test a variable which has no equivalent directive" – this is, unfortunately, one such case.

Before Diving Into The Specifics Each Conditional Statement, a Bit of Nginx Context is Needed

The origin of the phrase "if is evil" is because they work quite differently from how you'd expect - Which is the primary reason we need so many of them.

In general, you can assume two rules when working with conditionals in Nginx:

  1. Prior conditional modifications to the response object get cleared when you enter the context of a new conditional

  2. Conditional statements can't be nested or joined

This means if we want to test for two conditions then we have to use a rather odd structure where we test each condition individually (for example the value of a header and the method of the request), concatenating the result of the condition to a single variable that we can later check. Then by checking the value of this variable in a third test, we can discern the state of the component tests.

For example, an "and" would be implemented as:

if (a="b") {
    set $test  "A";
}
if (x="y") {
    set $test  "${test}B";
}
if ($test="AX"){
    return 200;
}

Let's Skip The Boilerplate and Focus on The First of These Conditional Statements

        if ($http_origin ~* "^http://www.example.com:8080$") {
            add_header Access-Control-Allow-Origin "$http_origin";
            add_header Access-Control-Allow-Methods "OPTIONS, DELETE, POST, GET, PATCH, PUT";
            add_header Access-Control-Max-Age "3600";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Allow-Headers "Content-Type";
            set $test  "A";
        }

This checks the 'Origin' header of any incoming requests, sets our `$test` variable to equal "A," and (if no other conditional statements pass) adds some headers to the response.

The Second Conditional Statement Then

if ($request_method = 'OPTIONS') {
            set $test  "${test}B";
        }

Checks to see if the request method is `OPTIONS` and appends `"B"` to our `$test` variable.

The Third Condition

        if ($test = "AB") {
            add_header Access-Control-Allow-Origin "$http_origin";
            add_header Access-Control-Allow-Methods "OPTIONS, DELETE, POST, GET, PATCH, PUT";
            add_header Access-Control-Max-Age "3600";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Allow-Headers "Content-Type";
            return 204;
        }

Checks if the first and second conditions passed by validating that both of the values we appended are present in the string in the order of execution.

Lastly, The Fourth Condition

        if ($test = "B") {
            return 403;
        }

Checks if the second condition was the only one that passed - essentially equivalent to `( (x=="y")  &&  (a!="b") ).` If this condition passes then it shortcuts to a 403 error.

This error condition is essential for the security of the resource that we are protecting, as it means that we're rejecting any request where the Origin header does not match our expected values.

Watch a demo to see StackHawk in flight

Further Reading

We have a growing number of language & framework-specific tutorials on how to implement CORS on your platform of choice so we won't go into code implementations in this article, you can find the ones we've completed here:

Missing your favorite platform? Drop us a message!

As linked earlier in the article, you can find our detailed explainer on CORS and what each of the headers controls here.

This post was written by Tim Armstrong. Tim has worn many hats over the years, from "Dark Lord of Network Operations" at Nerdalize to "Lead Software Engineer" at De Beers. These days, he's going by "Consultant Engineer & Technical Writer." You can find him on Twitter as @omatachyru, and at plaintextnerds.com.


Tim Armstrong  |  June 18, 2021

Read More

Laravel XSS: Examples and Prevention

Laravel XSS: Examples and Prevention

Rails XSS: Examples and Prevention

Rails XSS: Examples and Prevention

Java XSS: Examples and Prevention

Java XSS: Examples and Prevention