How to Enable CORS (Cross-Origin Resource Sharing) in HAProxy?

The HAProxy Cross-Origin Resource Sharing (CORS) Lua module streamlines adding CORS to your APIs. What is CORS? Read on to learn more.

It doesn’t matter whether you’re using Angular, React, Vue, or simple, vanilla JavaScript. You’re guaranteed to need to fetch or manipulate data on a server and for that, you’ll likely write a server-side API. Doing so decouples your frontend logic from backend server logic and leads to a more flexible and scalable system. However, trouble looms when you try to invoke your server-side API methods from JavaScript code that isn’t running on the same domain. For example, your API may be hosted at api.example.com, but your website that uses it may be at example.com. Or, maybe you host your API at port 8000, while your website listens at port 80?

Inexplicably, the request fails. You check your browser’s Console tab in the Developer Tools window and see a message like this:

Access to fetch at ‘http://api.example.com/dothing’ from origin ‘http://example.com’ has been blocked by CORS policy. No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

In its well-meaning attempt to protect you from malicious exploits, such as cross-site request forgery, the browser has denied an Ajax or Fetch request that originated from one domain from calling API methods on another. This same-origin policy is implemented in all major web browsers and, while it protects users from dangerous code execution, it stymies your efforts to assign different domains to your services. The solution is to enable Cross-Origin Resource Sharing (CORS). CORS lets you whitelist certain domains so that they can call your API methods.

How Does CORS Work?

CORS is a mechanism for whitelisting domains that the browser’s same-origin policy would have otherwise restricted. Without enabling CORS, the request flow looks like this:

haproxy cors diagram

Blocked by same-origin policy

JavaScript code running on a webpage makes an asynchronous request to an API URL. The server receives it and returns a valid response. However, when the browser sees that the domain, example.com, does not match the API’s domain, api.example.com, it blocks the response from being seen by the JavaScript code.

The reason is to protect you from malicious websites. Consider if earlier you’d visited your bank’s website. The code running there is allowed to call the bank’s web API to fetch details about your account and make requests to transfer money. The API requires that you be logged in so that it knows which account to work on, but that’s typically handled by storing a cookie in your browser after you’ve authenticated and including the cookie with the API calls.

Now imagine that you visit another website that, unknown to you, runs malicious JavaScript code that makes the same calls to your bank’s API, but with the twist that it attempts to transfer money to the bad guy’s account. If you still have the bank’s cookie stored, the requests would have succeeded since you’re still considered logged in, and the cookie will be sent with these requests too.

Thankfully, the same-origin policy blocks any JavaScript-initiated requests if they come from a domain that doesn’t match the API’s domain. This prevents malicious websites from imitating requests to your bank.

You may wonder, what good does it do to block a request after it has already reached the server and that server has returned a response? This portion of the same-origin policy protects visitors from malicious GET requests so that attackers can’t view data they shouldn’t have access to see. Modifying data, such as to transfer money, is protected by using a CORS preflight request, which asks prior to sending the request whether the client should be able to use POST, PUT, DELETE, and other non-GET methods. If not, then those requests will be blocked before the browser tries to send them.

Going back to our example, if the server had used CORS and attached the Access-Control-Allow-Origin header to whitelist example.com, then the browser would have allowed the response to be viewed.

allowed request by cors

Request allowed by CORS

The same-origin policy only applies to network calls initiated by client-side code. A cross-site request forgery exploit depends on the unsuspecting visitor to still have an unexpired login cookie in their browser. In other words, such attacks are useless without tricking someone into visiting a site. An attacker couldn’t use curl, for example. So, it makes sense that the browser does the enforcement. From the server-side, we only need a way to let the browser know which domains are permitted.

Using HAProxy as an API Gateway

Before diving into how you can enable CORS in HAProxy, there’s a use case that makes it especially useful. We demonstrated how HAProxy can serve as an API Gateway in our previous blog post, Using HAProxy as an API Gateway. An API gateway is a design pattern in which a reverse proxy is placed in front of your server-side APIs in order to manage cross-cutting concerns like authentication, monitoring, and rate limiting. Clients connect through the proxy to access your services and, in doing so, you receive several benefits, including:

  • the ability to load balance requests across multiple servers

  • client authentication (Basic, token-based, etc.)

  • improved security (rate limiting, whitelisting IP addresses, bot protection, etc.)

  • observability over connections, requests and their timings (real-time metrics and logging)

HAProxy provides all of these benefits when used as an API gateway, and it also comes with an integrated Lua runtime. So, you can use custom or off-the-shelf Lua modules to extend its functionality. You can learn more about extending HAProxy with Lua in our blog post, 5 Ways to Extend HAProxy with Lua.

You can enable CORS functionality in HAProxy by using the HAProxy CORS Lua Library. It adds the following:

  • When a request is received, it checks for the existence of an Origin header. An Origin header lets you know that the browser is expecting a CORS response or else it will block the request.

  • The module adds an Access-Control-Allow-Origin header to the response, which tells whether the client-side domain is whitelisted.

  • If the request is a CORS preflight check, then it adds an Access-Control-Allow-Methods header that contains the HTTP methods (e.g. GET, POST) that are permitted.

The basic gist is that CORS entails adding response headers that whitelist domains and/or HTTP methods. You may wonder why Lua is needed at all. Can’t HAProxy add and modify headers without it? Yes, and no. The HAProxy configuration language does provide a way to add, remove and modify HTTP headers. For example, you can manipulate headers like this:

frontend www
bind :80
# Add a response header
http-response add-header X-XSS-Protection "1; mode=block"
# Remove a response header
http-response del-header X-Powered-By
# Change a response header
http-response set-header Via "HTTP/2.0 haproxy1"
default_backend webservers

However, the specification stipulates that the Access-Control-Allow-Origin header should contain only a single domain, not a list of all of your whitelisted domains. Therefore, if you need to permit more than one, you’ll need a programmatic way of returning only the singular domain that’s being examined. That is the main purpose of the module and you’ll find if you inspect its code, that the implementation is very concise.

Note that some people prefer to use a wildcard (*) to whitelist every domain on the Internet as being allowed to access their API. That’s fine for some public APIs, but dangerous if the API shouldn’t be shared so widely.

Using the HAProxy CORS Module

HAProxy is compiled with Lua support. You can check which version of Lua by running the following command:

$ haproxy -vv | grep Lua
Built with Lua version: Lua 5.3.5

Download the HAProxy CORS Lua library and copy the cors.lua file from the lib directory to a directory of your choice, such as the /etc/haproxy. Then, edit your /etc/haproxy/haproxy.cfg file so that it has a lua-load directive in its global section, as shown:

global
lua-load /etc/haproxy/cors.lua

When a browser requires a CORS response, it signals this by including an Origin request header with a value set to the domain of the caller. For example, it would look like this:

Origin: http://example.com

Lua modules have access to either a request or a response, but not both at the same time. So, you’ll need to capture the value of this header and store it for later, when the response is returned.

The CORS Lua module provides a one-liner that will create a variable behind the scenes and store the header: Add an http-request lua.cors line to your frontend or listen section. This line captures and stores the Origin header so that the Lua module has access to it during the response phase. Also, add an http-response lua.cors line to the same section:

listen api
bind :80
http-request lua.cors "GET,PUT,POST" "example.com" "*"
http-response lua.cors
server s1 192.168.50.20 check
server s2 192.168.50.21 check
server s3 192.168.50.22 check

The http-request line’s first parameter lists the HTTP methods that are permitted. The module will add these to an Access-Control-Allow-Methods header in response to a CORS preflight request.

The second parameter is a list of domains to whitelist. In this example, it’s set to example.com. You can add more domains, separating them with commas. The third parameter is a list of permitted custom HTTP headers.

Did you know?

The same-origin policy considers even a different port to be a different domain. So, calling a service that is listening at localhost:8080 will be blocked if the call originates from localhost:80.

With this configuration in place, client-side code running in the browser at example.com can successfully call the API methods, even though they’re hosted at api.example.com.

Enabling CORS in HAProxy: Conclusion

In this blog post, you learned how HAProxy comes packaged with a built-in Lua runtime. You can use this to extend HAProxy by using either custom or off-the-shelf Lua modules. One such module is the HAProxy CORS Lua library, which enables you to send CORS response headers as needed. CORS lets you whitelist certain domains that would be blocked by the same-origin policy.

Want to stay up to date on similar topics? Subscribe to this blog!

HAProxy Enterprise combines HAProxy Community, the world’s fastest and most widely used, open-source load balancer and application delivery controller, with enterprise-class features, services and premium support. It is a powerful product tailored to the goals, requirements and infrastructure of modern IT. Contact us to learn more and get your HAProxy Enterprise free trial!

Subscribe to our blog. Get the latest release updates, tutorials, and deep-dives from HAProxy experts.