Blog

All Blog Posts  |  Next Post  |  Previous Post

TMS Web Core and More with Andrew:
Fun with CORS

Bookmarks: 

Monday, June 5, 2023

Photo of Andrew Simard
As developers of modern web applications, we're often expected to know quite a bit about a range of obscure topics just to get through a typical day. With acronyms for everything spilling out everywhere. It can be a mess at the best of times. And just when you think you've got a handle on everything, CORS comes knocking, just to ruin your day. This time out, we're going to have a look at what CORS is all about, particularly from the perspective of a TMS WEB Core developer. Much of the time, CORS is very deserving of all the bad press, but at the same time, the challenges it presents are often readily solvable. And when they aren't, various workarounds are possible, sometimes making the whole affair seem a little contrived.


What is CORS?

If you've not run into it yet, CORS usually presents itself as an error in your web application that comes up when you're trying to access data from a remote source. It's an acronym, of course. Cross-Origin Resource Sharing. In its simplest form, what it refers to is a security feature that comes into play when a web application hosted on one website tries to access content hosted on another. The "origin" in this case refers to the website that is hosting the original application. Cross-origin refers to how it is trying to access something somewhere else. And the resource sharing bit is just describing the general intent around accessing files of some kind by multiple clients. So that's not all that helpful.

Let's say we have a TMS WEB Core app that has been deployed using an Apache web server on a website called app.example.com. Let's say our app wants to access some files, maybe some images for example, that are stored on a different Apache or XData web server. Let's call that one data.example.com. It doesn't really matter which web servers are being used in either case. The issues are the same. When our app contacts data.example.com, it passes along a bit of information, including the fact that it was launched from the app.example.com website, what it refers to as its "origin". 

Depending on how the data.example.com website is configured, it may decide whether to allow access to whatever our app is asking for, basing its decision around this "origin" value. If nothing is configured, such access is automatically denied. Much of what we'll be covering in this post relates to that decision mechanism and how we can make changes to it to gain the access we need. And if we can't do that, then we'll look at how to avoid being in that position in the first place.

CORS is by no means a new problem. It has been around for some time, causing trouble all the while. In fact, here are a couple of CORS-related posts, one from this very blog originally from 2015, and one from the TMS Support Center, just in the past month or so. Just an example of how prevalent CORS has been, even in this corner of the internet. Good material to review before continuing.

When TWebHttpRequest is not working as expected
CORS and pre-flighted requests with XData


It's Not Me, It's You.

So the first big takeaway is that CORS is effectively a server-side security mechanism. If CORS is not properly configured on the server, then that's a pretty good place to focus our attention. There are situations where we can't do anything about the server, so we'll cover that a bit later. This also means that there generally isn't any kind of configuration or option or setting anywhere in our TMS WEB Core project that can actually help resolve the problem. The problem isn't with our app, but rather with the other end of the equation - the server we're contacting.

Importantly, this is partly where it gets its bad reputation. When we run into CORS issues, often it is only our app that exhibits the problem. So the thinking naturally is that there's something wrong with our app that we can change to fix the problem. Which most often actually isn't the case.

But how do we know if the problem is with CORS or with something else? Well, CORS deservedly gets blamed for a lot of problems, because it indeed is the source of many of them, but it isn't the cause of all of the problems. We should be very careful to rule out other potential problems first before diving head-first down the CORS rabbit hole. 

And, as it turns out, that's pretty easy to do. Let's say that we want our app running on app.example.com to download an image from data.example.com. Maybe the URL is something like https://data.example.com/photos/camel.jpg. If we're encountering an error in our app, the first and most obvious thing to try would be to plunk that URL into a browser and see if it works.  

An important point here, another big takeaway, is that CORS is applied to browser requests coming from an application (aka JavaScript, typically). This is a security mechanism enforced in part by the browser. Other applications, like Delphi VCL applications, or even just entering a URL into a browser, are not subject to the same rules. A URL entered into certain elements, like a <video> or <audio> tag, is also potentially not subject to the same rules. This is more about JavaScript access than anything else. There are reasons for this, which we'll get into shortly. But this means that if we just use a URL to test if a resource is accessible, it isn't using CORS at all. 

So why is this important? Well, it means that we can test whether there are any other problems that are blocking our access to the resource - an image in this case. And there are lots of things we can rule out with such a test, all of which have nothing to do with CORS. So, if entering the URL in the browser doesn't get us the expected image, then potentially there is something else afoot that likely means that our app won't be able to access the image either, regardless of CORS.  

Here are some examples of why a URL might not work. If it does work, we've ruled out all these problems. If it doesn't, then addressing the problem here might solve our app access issue without even having to think about CORS.

  • The URL could have a typo in it. Really basic problem to have, but also pretty common. Be sure to check whether your URL has any elements that are mixed case, invalid URL characters like spaces, and that the hostname is correct. Sometimes when accessing other resources, a different hostname might be used, like photos.example.com or api.example.com rather than www.example.com.
  • Check the protocol - HTTP vs. HTTPS. Any public-facing production server should be accessed only by SSL these days, but there are still a few stragglers. There can be problems with "mixed content" - data coming from both HTTP and HTTPS sources displayed on the same page, so best to get everything shuffled over to the HTTPS side if you have any say in the matter.
  • Check that HTTPS is properly configured. SSL certificates expire all the time, and even some of the largest organizations have overlooked renewals from time to time. Setting up a server with an SSL certificate is also not entirely free from problems, so be sure to check that your server has a valid SSL Certificate. For XData servers, this means using the TMS HTTP Config Tool to assign an SSL certificate to a particular URL/port on the system. For Apache, this involves setting up specific sections of the configuration to point at where the SSL certificate files might be stored. Ideally, these would be configured in a way that auto-renewal is as automated as possible.
  • Server firewall settings can certainly interfere. If you've just configured a new Apache web server, a new XData server, or something along those lines, or changed a port number, be sure to check that the firewall running on the system is configured to allow access to it. It can also be the case that the firewall blocks certain IP addresses, so be sure that the system you're using to test isn't blocked by adding it to a whitelist if automatic blocking is enabled.
  • Client firewall settings aren't usually the cause of interference, but depending on what you're doing, they can be. Antivirus software and other desktop-protection-style tools can introduce all kinds of blocks that aren't immediately obvious. This is often the case, particularly when dealing with mail and SMTP, but it can also be an issue when accessing web content on unusual ports.
  • Accessing HTTPS sites in the local network can sometimes be a problem. If you've got a server configured in your network that is accessed through your router via NAT, for example, this often doesn't play very nicely with HTTPS access. This is due to most routers not allowing connections to flow back into the network through the NAT interface when they originate from the local network. The best approach here is to access local servers via HTTP and remote servers via HTTPS. And if you need to test a local HTTPS server, try to do it from a remote system outside the local network.
  • Check that the server application is actually running. This can happen a lot if you're flipping back and forth between a TMS WEB Core project and a TMS XData project, and you forget to leave the XData project running. This can also happen with Apache, as it just takes a small typo in one of its endless configuration files to prevent it from starting up.
  • Check that the server has access to serve up the image. Apache running on Linux has layers of security that determine what it can and can't serve. Often, permissions for a directory tree need to be configured appropriately for the Apache server to have access to the files.  
  • Check that the server is configured to allow serving the file types you're interested in. There are settings within Apache that can be configured to restrict access to all but the most commonly used files. In a properly-secured Apache server, in fact, it isn't going to allow access to anything unexpected. So if you've got files with an unusual extension, it might be that you have to do some configuring for those to be served. Or maybe even if the extension isn't so unusual, like .json or .csv.
  • Often, access to a resource is restricted somehow, maybe requiring an authentication token like a JWT, or some other API key. This makes it a bit harder to test with a browser as there may not be any place to stick such a token. But we've got options here. Using the command-line 'curl' command, we can pass the authentication information using the -H parameter (header). If we're using XData with Swagger enabled, we can also use that to enter authentication information, assuming that it is an XData endpoint that we're trying to access. TMS recently released their free REST Insight tool which can be used in a similar way, great for when you're accessing someone else's server or when you don't have Swagger enabled. 

Ideally, you should be able to largely mimic whatever your app is doing to get access to the remote content. The good news is that if your URL works, you can check off all of them at once. And there are no doubt other items that could be added to this list. If anyone posts a comment about something obvious I've overlooked, I'd be more than happy to update the list.

The purpose of this browser test, though, is to ensure that you've actually got a CORS problem and not something else hiding behind an obscure error that might be easily mistaken for CORS. If you can't get access to the resource outside of your app through some mechanism like a browser or curl or a REST interface, there's little chance your app will be able to get access either, hence why it is important to do this most basic check first.

So You Think You've Got a CORS Problem.

Assuming that you've tested and were able to access the resource without problems using the browser or curl or a REST interface, but your app still can't connect, then it may indeed be that you have a CORS problem. What does a CORS problem actually look like? Usually, it is just an obscure error in the browser console, and it can sometimes be very misleading. Here are a few examples.

TMS Software Delphi  Components
Example #1.

In this case, we get a "blocked by CORS policy" but it is entirely misleading. The reason this error is generated is actually because the resource simply isn't available. This is an error generated from our recent Icon Picker blog post when using the Font Awesome icons. In this case, a slew of Font Awesome icons are downloaded after performing a search. Some of the icons returned by their API, however, refer to icons that no longer exist.

In this case, a "penny-arcade" icon used to be included in Font Awesome 5, but is not part of Font Awesome 6. So they've got an error in their API, essentially. This error, then, is a bit of a nuisance that really has nothing to do with CORS. It is ultimately just a "file not found" error and can be safely ignored. Sure isn't very friendly, though.  Even less so as it is repeated for each icon that is missing in this fashion.

TMS Software Delphi  Components
Example #2.

This is one we might very well encounter more often. If we create a TMS WEB Core app (running on the default TMS Web Server port 8000) and use it to try and connect to a TMS XData server on our same development system (in this case, running on port 12345), and CORS has not been enabled in XData, this is the error that will result.

It is not so simply stating that your app, running on http://localhost:8000 (our equivalent of app.example.com) is trying to access a resource on a web server running on http://localhost:12345 (our equivalent of data.example.com). And because CORS has not been configured, this fails. It tries to suggest an alternative - passing a header that includes 'mode: no-cors' but this is unlikely to be what we want to do. The "opaque" response means that our app won't be able to read the data in that case. So not very useful.

TMS Software Delphi  Components
Example #3.

Here, we see the same CORS error when we try to do a fetch() from the browser console. The two servers listed are slightly different. The browser was showing the Actorious website at the time, so when any JavaScript is run from the browser, this becomes the 'origin' of the request, at least until we visit another website. The other two error messages are just compounding the problem. Note that just entering the very same URL into the browser's location bar works perfectly fine, returning JSON that includes all the Icon Sets from that project. 

And that's where a lot of the frustration comes from. Copying and pasting the URL from our fetch() command works fine in the browser, but the exact same URL accessed via JavaScript fails spectacularly. This is when we know we have a CORS problem - working outside JavaScript but not inside JavaScript.

Server Configuration Accessible.

If we've identified a CORS problem with our app, the next obvious step is to try and figure out what to do about it.  As we mentioned earlier, this is most often a server configuration issue. How difficult it will be to resolve depends largely on whether you have any control over the server in question. Assuming the server configuration is accessible, we can hopefully make quick work of this. If the server configuration is not accessible (either by you, your ISP, or someone in your organization) then we've got a bigger problem. We'll address that in the next section.

What is it that needs to be configured? That's where the error message is actually helpful. There is something called the 'Access-Control-Allow-Origin' header that is responsible for all of this. To be clear, there is more to CORS than this one header value, but it is all we'll need to be concerned about at the moment. This is a header value that is returned in the response to a web server request

Our app (or the browser on behalf of our app) makes a request of the remote web server, passing in information in its own header, like the 'origin' value and 'content-type' that describes what we're sending. The remote web server checks over our request and sends back a response. If the response doesn't include the 'Access-Control-Allow-Origin' header, and this request is being sent to a different remote server than what our own 'origin' is, then the request will fail - the browser will block the request.

Making this more complicated, this exchange can happen more than once. In all but the simplest of requests, the browser will send a "preflight" request using an "OPTION" method (as opposed to the GET or POST method of the anticipated request). This preflight request is performed to check if the actual request, where data is being transmitted, is actually permitted. And it is usually here that the 'Access-Control-Allow-Origin' situation first appears.

What can we do about it? Plenty. The most obvious thing is to simply configure the server to add the 'Access-Control-Allow-Origin' header to the responses it is sending back. There are various rules here, but generally, this header will have one of two values. If you, as the server owner, don't much care who's making requests, and you're not using HTTP authentication, then this header can be set to an asterisk, and we can call it a day. 

If you do care who's making requests, then this can be set to match the origin of where those requests are coming from, like app.example.com. This means that only our app can access the resource in question. This can be a bit of a problem if you have multiple applications connecting to the same resource and they change frequently, but something to keep in mind. 

XData Configuration.

If your server is an XData server, this is not too difficult to configure at all. In the default XData project, there is a ServerContainer unit (Unit1.pas) that has the XDataServer component. This is where CORS can be configured.

  1. Open the ServerContainer Unit (Unit1.pas).
  2. Right-click on the XDataServer Component and select "Manage middleware list". Double-clicking has the same effect.
  3. Right-click on an empty part of the list window that appears, and select 'Add middleware'.
  4. Select CORS from the available middleware choices.
  5. Select CORS in the middleware list after it has been added, so that the Object Inspector shows its options.
  6. Enter an asterisk into the Origin property.

TMS Software Delphi  Components
XData CORS Configuration.

There are some other great middleware components in there as well. In particular, Compress is another option that could well be included most of the time. With CORS added, building and running the XData server app is all that is left to do. Now, when a request comes along, the 'Access-Control-Allow-Origin' header will be present, populated with whatever value you entered into the Origin property in the Object Inspector. Easy, right?

Configuring Apache.

If your remote web server is running Apache, then adding in the 'Access-Control-Allow-Origin' header is mostly a matter of finding the best place to put it. The code that you need to add is the following.

<IfModule mod_headers.c>
  Header set Access-Control-Allow-Origin "*"
</IfModule>

Depending on what kind of Apache configuration is involved, the best place for this might very well be within the <VirtualHost> directive. If it is just a particular folder that needs to be configured in this way, a <Directory> directive might be another good choice or even an .htaccess file. Or the main httpd.conf file (sometimes called apache2.conf) if this is to be applied across everything managed by Apache. It may be necessary to restart Apache or at least instruct it to reload its configuration files. None of this should be particularly difficult if you're already familiar with how to configure Apache. If this isn't familiar territory, there are plenty of online resources to help. In fact, there's an entire website dedicated just to enabling CORS in various web servers: Enable CORS.

There are plenty of other CORS-related headers and other functionality that can be configured using this same approach, but setting this one header should at least get things moving again.

Server Configuration Inaccessible.

What if the remote web server isn't one that you control? Let's assume that it is owned by someone else, and they're not taking your calls. How do we get the request to the remote server in a way that the browser will accept?

One approach is to use a CORS proxy. What is a CORS proxy? It's a separate web server (or web service might be more accurate) that takes your remote URL and makes the request on your behalf, passing back the response. A CORS proxy isn't a web browser, so it isn't bound by the same rules that a browser is. It can simply ignore whatever the 'Access-Control-Allow-Origin' situation is, and make requests regardless. It then returns the data to your browser with the necessary CORS headers so that your browser thinks everything is just fine. Almost makes CORS seem like a waste of time!

There are freely available CORS proxy services. Be mindful of these, however, as the proxy service will be able to see everything passing through it. You wouldn't want to use such a service for anything that was in any way meant to be private. This includes anything using an API key or other credentials. It might work in a pinch, but this isn't really a production solution to any CORS problem.

Running your own proxy using one of the GitHub repositories, like cors-anywhere, might be a workable solution.  In this instance, you're running essentially another web server of your own, and any requests destined for a CORS-deficient remote server can instead be passed to this proxy server. It will respond in the same way, with the necessary CORS header information. The main risk here is ensuring that this is configured only for your own use. 

Some of the popular configurations are designed to make it easy to pass any request through the proxy and have it bypass the CORS situation, by just including the entire URL as part of what is sent to the proxy. Some care should be taken to ensure that only your applications are permitted to use this proxy, using, in the case of cors-anywhere, the available whitelist feature and other configuration options.  

Another option, if you're already using XData in your project, is to use an XData endpoint as a CORS proxy.  This endpoint can be fashioned in such a way that it takes your request as a parameter, makes the request on your behalf, and returns whatever results it receives. It's a proxy, after all - that's what proxies do. Similar to the cors-anywhere solution, this is something that would run on your own systems. And you can even add your own security - maybe it can only be used by those who are logged in. Just add the [authorize] attribute in the endpoint's interface declaration, and you're all set. So long as you've already set up the necessary JWT middleware with a corresponding login function. The endpoint is then very simple. You could also have a whitelist of IP addresses or IP names and check that the request is coming from one of those before continuing.

In our Icon Picker example, we set it up to just have the Font Awesome request passed in. But it could be extended to also include the remote URL. Something like the following.

function TSystemService.CORSProxyPost(URL: String; ContentType: String; Body: String): TStream;
var
  Client: TNetHTTPClient;
  DataStream: TStringStream;
  Response: String;
begin
  DataStream := TSTringStream.Create(Body);
  Client := TNetHTTPClient.Create(nil);
  Client.Asynchronous := False;
  Client.ContentType := ContentType;
  Client.SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12];
  Response := Client.Post(URL,DataStream).ContentAsString;
  Result := TStringStream.Create(Response);
  Client.Free;
  DataStream.Free;
end; 

Now, this can be called just by passing the URL and whatever Content-Type and Body that need to be included.  Whatever is returned by the remote server is sent back as a TStream. This is ideal when you're sending JSON to a remote server and expecting JSON back. Other parameters could be passed if custom headers were needed, like an authorization header, for example. Just be sure to enforce some kind of access control so that it doesn't get used by anyone else. And of course, be sure to enable CORS for this XData server.


Wrap-Up.

With CORS having various workarounds available, what was the original intent behind it? Prior to CORS, web applications were blocked from accessing remote content, just as we find now when CORS headers are not provided. This was a security protocol that was intended to prevent malicious websites from stealing user data.  But that's an ongoing battle, as new and creative ways to infiltrate websites and steal user data are a continuing threat. As more aspects of our lives effectively move online, the number of bad actors, and their levels of sophistication, continue to escalate.

For our purposes, we're usually about trying to get things to work, not so much about the gritty details of security.  Not because it isn't important, but rather because if our app doesn't work, all the security in the world isn't going to make a difference. And that's a whole other topic for another day. More acronyms! Here's what ChatGPT had to say in response to "What security risks does CORS address?":

CORS (Cross-origin Resource Sharing) addresses various security risks, especially related to cross-site scripting(XSS) and cross-site request forgery(CSRF) attacks. By allowing cross-domain requests to be made in a controlled, secure, and standardized way, it helps mitigate the following risks: 

1. XSS(XSS): Cross-site scripting can occur when an attacker injects malicious scripts into a web application. It is a significant security risk as these scripts can steal sensitive user data or even hijack user sessions. CORS helps prevent XSS attacks by restricting cross-origin requests that could access sensitive data based on the same-origin policy.

2. CSRF: CSRF attacks happen when an attacker tricks a user into performing actions on a website or web application without their knowledge. CORS addresses CSRF risks by specifying which domains are allowed to submit a particular request and which HTTP method can be used.

4. Protecting user credentials: CORS prevents unauthorized parties from accessing protected user information by requiring the use of specific HTTP headers for requests that contain user credentials. In summary, CORS helps prevent unauthorized access to resources and data across multiple domains, thereby mitigating Cross-Site Scripting and Cross-Site Request Forgery attacks.

Not sure what its third point was? Hopefully it wasn't too important! Regardless, the point here is that there is a lot more to the story than what we've covered. There are more CORS options, various rules that are applied in certain conditions, and even a few performance-related tweaks that can be employed (eg: caching preflight checks). But if you're stuck on a CORS issue in your TMS WEB Core project, by all means, please post a question to the TMS Support Center. Or, check out this website, one of the more pleasant sites covering CORS information: Will it CORS?

As always, questions, comments, and feedback are much appreciated.


Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.



Andrew Simard


Bookmarks: 

This blog post has received 4 comments.


1. Thursday, June 8, 2023 at 12:33:12 PM

9 more blogs and you''ll be above Wagner! Seriously, unfortunately CORS is different on each platform.

Randall Ken


2. Thursday, June 8, 2023 at 12:37:18 PM

Very soon! Five posts next week, in fact.

CORS can be a headache, but was this post helpful? What kinds of differences or issues have you run across that I might very well have overlooked?

Andrew Simard


3. Friday, June 9, 2023 at 4:15:39 AM

One of my servers uses Plesk and the only way I managed to get CORS working on it was to configure the additional Apache headers within Plesk itself.

Randall Ken


4. Friday, June 9, 2023 at 7:14:28 AM

Ah. I had provided a link to the https://enable-cors.org website which covers Apache, but many people only have access to their server configuration through a front-end tool like Plesk or cPanel, which is a good point I should have made. I personally use VirtualMin for this sort of thing, but I also have direct access so I did not think much about it, as it is ultimately still Apache that is getting configured.

Andrew Simard




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post