So I was working on some web view and I needed to send an XMLHttpRequest using JS. Usually I was working with my own backend, but this time it was a different remote host (our YouTrack instance), and my request failed with the following error:

Firefox, missing CORS header
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://some.host?params=ololo. (Reason: CORS header Access-Control-Allow-Origin missing).
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://some.host?params=ololo. (Reason: CORS request did not succeed).

That’s how I learnt about the existence of CORS, as it turned out I never sent cross-origin requests before.

What is CORS

But first, what is SOP

Before CORS one should know about Same Origin Policy (SOP). It’s a mechanism that prevents JS scripts from accessing resources (such as REST API) on other websites.

And it is a very nice-to-have security measure. Let’s say you’ve logged-in to your online bank, did some things there, and then went to watch some pron. Sadly, you landed on a cheeky website, which contained certain evil JS-script, which might theoretically (and in past it actually could) perform certain actions that you were unlikely to like, but, as MDN article says:

...SOP prevents a malicious website on the Internet from running JS in a browser to read data from a third-party webmail service (which the user is signed into) or a company intranet (which is protected from direct access by the attacker by not having a public IP address) and relaying that data to the attacker

Which means that your web-browser (if you are not using some SuP3rM3gACusT0M.VASYAN-ED1T1ON.v9000 version) will prevent the execution of such requests to your bank’s website (or REST API), because the origin of your bank website is different from the origin of this malicious pron website.

So hopefully you can now see for yourself how stupid is the idea of disabling SOP in your browser. And yet, some “experts” on some forums actually suggest to do that in order to “fix” the “problem” with CORS.

And now about CORS

CORS stands for Cross-Origin Resource Sharing. And it’s a quite common misunderstanding that CORS is also a security mechanism that prevents XMLHttpRequests from being executed.

Actually, it’s the other way around - CORS makes it possible to weaken the SOP (or, rather, make exceptions) by allowing trusted origins to access resources on certain origin, in particular to send XMLHttpRequests to it. Here’s a good article about that.

Now coming back to our problem. So, okay, the scenario from above is about malicious scripts, but in our case we are merely want to call API of some service (YouTrack instance), so how to “enable” this CORS to allow the requests?

And here’s answer - you cannot do anything on the client side (JS-script on your page) to make your XMLHttpRequests reach the target host of a different origin. There is nothing wrong with your JS code, you didn’t forget to enable some flag, you cannot whitelist target domain - nothing, there is nothing to do about the issue on the client side. Don’t waste your time googling possible solutions and don’t even think about disabling SOP in your browser, as not only it is retarded, but also it would mean that all your users would need to do that as well (which brings it from retarded to super-retarded level).

Here’s why: trusted origins are set on the server side, and it’s a web-server or application who returns a special Access-Control-Allow-Origin header, which should contain the origin (and preferably not a wildcard *) from which you are allowed to reach them.

In my case it was simple - YouTrack has a setting exactly for this purpose:

YouTrack allowed origins

So I allowed my http://localhost:5000 origin, and requests started to work. Obviously, you’ll also need to add the origin of your actual domain from where your scripts will run in production.

If other cases it might be a similar setting somewhere in the service settings, or a corresponding line in NGINX/Apache/etc config.

Here’s how a successful XMLHttpRequest looks like:

Firefox XMLHttpRequest headers

As you can see, the browser added Origin: http://localhost:5000 header (browser does it by itself for all cross-origin requests), and server returned Access-Control-Allow-Origin: http://localhost:5000 header, which means that the request is allowed to be sent.

There is more to it, as requests can be simple and preflighted. The same article has a shorter explanation.

Blockers

I said that requests started to work after I added my origin to the list of allowed origins on server. And they did, but not right away.

At first they were still failing, and the reason for that was my uMatrix browser extension:

uMatrix blocking XHR

That’s totally my fault because I should have thought about that. Enabling XHR solved the issue.

Requests implementation

As a bonus, here’s an implementation of querying YouTrack API for getting a list of issues.

JS

function getIssues()
{
    let xhr = new XMLHttpRequest();
    let params = {
        "filter": "project: SomeProject State: -Fixed, -{Can't Reproduce}, -Duplicate, -{Won't fix}, -Incomplete, -Obsolete, -{Not a bug} Stable release: SomeRelease",
        "max": "500",
        "with": "id",
        "with": "summary"
    };
    xhr.open(
        "GET",
        "http://youtrack.some.host".concat("/rest/issue", formatParams(params))
        );
    xhr.setRequestHeader("Accept", "application/json");
    xhr.setRequestHeader(
        "Authorization",
        "Basic ".concat(btoa("user:password"))
        );
    xhr.send();
    xhr.onload = function()
    {
        if (xhr.status != 200)
        {
            alert("[ERROR] Couldn't get issues");
        }
        else
        {
            // parse xhr.response
        }
    };
    xhr.onerror = function()
    {
        alert("[ERROR] Couldn't send a request for issues");
    };
}

function formatParams(params)
{
    return "?".concat(
        Object
            .keys(params)
            .map(function(key)
            {
                return key.concat("=", encodeURIComponent(params[key]))
            })
            .join("&")
    )
}

With a properly set CORS policy (allow your origin on the target server), it will work just fine. There is, however, a drawback - everyone can see credentials for your YouTrack instance in the page’s HTML source.

C#

Here’s another solution for the CORS/SOP problems - don’t send requests from JS frontend! SOP exists only in web-browsers, remember? So send them from backend, and give JS frontend only result data. That way you will also avoid exposing your YouTrack credentials in the page’s HTML source.

I am using .NET Core application, so here’s a C# implementation:

public IActionResult Issues(string releaseName)
{
    Dictionary<string, string> issuesList = new Dictionary<string, string>();
                    
    var filter = new StringBuilder("project: SomeProject ")
        .Append("State: -Fixed, -{Can't Reproduce}, -Duplicate, -{Won't fix}, ")
        .Append("-Incomplete, -Obsolete, -{Not a bug} ")
        .Append($"Stable release: SomeRelease");

    try // get the list of issues
    {
        var parameters = new List<KeyValuePair<string, string>> {
            new KeyValuePair<string, string>("filter", filter.ToString()),
            new KeyValuePair<string, string>("with", "id"),
            new KeyValuePair<string, string>("with", "summary"),
            new KeyValuePair<string, string>("max", "500")
        };
        var results = QueryYouTrack(
            HttpMethod.Get,
            "/rest/issue",
            parameters
            );
        if (results.Item1 != 200)
        {
            throw new Exception(
                $"YouTrack response: {results.Item1} - {results.Item2}"
                );
        }
        else
        {
            var rezJson = JObject.Parse(results.Item2);
            foreach (var issue in rezJson["issue"])
            {
                issuesList.Add(
                    issue["id"].Value<string>(),
                    issue["field"][0]["value"].Value<string>()
                    );
            }
        }
    }
    catch (Exception ex)
    {
        _logger.LogError($"Couldn't get YouTrack issues. {ex.Message}");
    }

    ViewData["IssuesList"] = issuesList;
    return View();
}

private Tuple<int, string> QueryYouTrack(
    HttpMethod method,
    string query,
    List<KeyValuePair<string, string>> parameters
    )
{
    using (var httpClient = new HttpClient())
    {
        httpClient.BaseAddress = new Uri(
            _configuration.GetSection("YouTrack:Host").Value
            );
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue(
                "Basic",
                Convert.ToBase64String(
                    Encoding.Default.GetBytes(
                        _configuration.GetSection("YouTrack:Credentials").Value
                    )
                )
            );

        HttpRequestMessage request = new HttpRequestMessage(
            method,
            $"{query}?{new FormDataCollection(parameters).ReadAsNameValueCollection().ToString()}"
        );
        
        var httpResponse = httpClient.SendAsync(request).Result;
        var httpContent = httpResponse.Content.ReadAsStringAsync().Result;
        
        return new Tuple<int, string>((int)httpResponse.StatusCode, httpContent);
    }
}