Crossing the Site Domain with JavaScript

Browsers try to prevent a range of malicious attacks by preventing content being accessed by a web page from a different domain to the one that the page was fetched from. If you have a legitimate need to do this, it is a bad idea to disable this method of defence: Instead, there are more legitimate and safer ways of performing cross-domain JavaScript calls such as JSONP or Cross-Origin Resource Sharing, as Dino explains.

It has become an easy task to expose data through HTTP endpoints. For example, if you’re on the Microsoft web stack you can use the controllers of a plain ASP.NET MVC web site or perhaps you can build a Web API frontend within an ASP.NET application hosted on IIS. It is quick and easy to call into these endpoints from the client side using, say, the facilities of the jQuery library. So where’s the problem?

The problem is that client-side HTTP calls-globally known as Ajax calls-are subject to the jurisdiction of client browsers; client browsers unilaterally decided, for security reasons, not to allow outgoing calls that reach out to a domain that is different from the domain where the current page was downloaded.

It is the Same-Origin Policy (SOP) that browser vendors added in relatively recent times to reduce the attack surface area for malicious users to zero. In this article, I’ll present in this article various techniques you can legitimately use to make selected content of a web site accessible from outside the domain.

Should You Disable SOP?

Your first instinct may be to disable SOP. The SOP can, in fact, be disabled on the client side, but that is a task that only end-users can perform. There is no control over the SOP that can be exercised from the server side. Therefore, as the author of a web page, you would only be able to let browser users know that they might need to disable browser’s web security in order to use the page. This is not really an easy or comfortable thing to do. Anyway, it is the browsers that let users disable web security but each browser does it in an entirely different way. For example, in Chrome it happens through the command line:

The disable-web-security parameter is all that matters to let the Chrome browser place cross-domain script calls. The allow file-access-from-files parameter only refers to the ability to access files referred through the file:// protocol, a feature somehow related to SOP. Internet Explorer users can achieve the same via the Settings dialog box, as in the figure below.

2433-1-cebde362-a0e6-480b-b598-0450938e4

Yes, it would be very difficult to provide instructions on turning off SOP for all browsers, and then to keep these instructions up-to-date. It turns out that relying on reduced browser security to have JavaScript cross-domain Ajax calls work is neither realistic nor recommended. You would need to have full control over the browsers being used by all your end customers. You also leave your customers exposed to several security vulnerabilities.

SOP generically calls out to web security and having such calls blocked by default undoubtedly makes your browser and Internet environment more secure.

If disabling the SOP is out of the question you still have a problem because there are as many scenarios in which a cross-domain call is just what you need and can be totally secure. A common example of this is when you just want to download some JSON data from another web server that you own and control. To work around the problem, you have three main routes.

  • JSONP or JSON with Padding
  • CORS or Cross-origin Resource Sharing
  • Handmade Proxies

Let’s find out more details.

The Weird Case of the SCRIPT Element

It is a curious fact that although a call that is placed via JavaScript to download data from external domains can be denied by the browser, no browser raises any objections if the data is downloaded from external domains via the SCRIPT HTML element. There is a good reason for allowing limited cross-domain access: As it stands, every time you download a script file from a Content Delivery Network (CDN) you’re actually downloading content from a domain that is not the same domain origin of the current page. And there’s more to it. The SCRIPT element is not the only HTML element that works beautifully in a cross-domain fashion. The same happens with IFRAME, LINK, and IMG. However, LINK and IMG elements are restricted to download specific data such as just CSS or only image data. The IFRAME element is further restricted in its ability to access and process the downloaded content. The SCRIPT element, in contrast, is flexible enough to serve as the ideal tool for picking the security locks of SOP.

The SCRIPT element is the foundation of the JSONP approach to performing cross-domain JavaScript calls. The expanded name of JSONP-JSON with Padding-explains the trick quite well. JSONP consists of the technique of instructing the remote endpoint that holds the JSON data to return some content padded in a JavaScript function call. The calling page will then host a SCRIPT element that points just to the JSONP-enabled endpoint. The net effect is that, when the browser processes the SCRIPT element, it places a call to a cross-domain endpoint and downloads whatever comes from there. The JSONP-enabled endpoint, though, doesn’t return plain JSON data. It returns, instead, a small chunk of JavaScript in the form of a JavaScript function call that wraps up the expected JSON data as an argument. The wrapper JavaScript function is defined in the calling page and contains just the logic necessary to process the downloaded JSON data.

JSONP works beautifully and is probably easier to demonstrate in practice than it is to explain in words. Before going any further with a demo, though, it must be remarked that JSONP requires strict cooperation between the calling page and the server endpoint. You can only use JSONP to call a HTTP endpoint that fully supports it and dictates the details of how to actually perform the call. Let’s find out more.

JSONP in Action

The jQuery library contains facilities to perform JSONP calls. However, let’s hold off on it for the moment and learn JSONP the hard way. All you need to have in the calling HTML page is the following:

The src attribute points to a HTTP endpoint known to be a JSONP-enabled endpoint. Let’s assume the endpoint belongs to an ASP.NET MVC application. Here’s a possible implementation for it.

Firstly, the HTTP endpoint retrieves the raw data it has to return. Next, the raw data is serialized as JSON. In the code snippet I use the JavaScriptSerializer class, but you can use any other class and framework you wish. In particular, you might want to use the NewtonSoft JSON framework. Anyway, be aware that JavaScriptSerializer is the class used by the Json helper method of ASP.NET MVC.

The most interesting step is the last one. The JSON string is packaged as the argument of a JavaScript function name. In the code snippet, JSONP_FUNCTION_NAME is a plain string constant expected to contain the name of the JSONP JavaScript function that will handle the JSON data on the client. Let’s say we have the constant defined as follows:

Any SCRIPT element that points to the CountriesP endpoint will ultimately receive the following script:

Hence, the browser will try to locate a JavaScript function called callback in the context of the DOM. If no such a function is found, you get a runtime error: Otherwise, the function is invoked and passed the JSON string.

The name of the JSONP function is usually defined by the JSONP endpoint and it is exposed as part of the API of the host web site. However, an even better approach for the author of the JSONP endpoint is to let the caller pass the name of the wrapper function. To add this feature to the CountriesP endpoint, you just add an extra parameter.

And now let’s get back to the jQuery library and its facilities for JSONP calls. Here’s how you call a JSONP endpoint using jQuery.

The jsonpCallback jQuery parameter sets the name of the JSONP function to wrap the return data. This is the scheme you follow when the endpoint uses a fixed function name. If the HTTP endpoint makes it parametric, then the name will be typically appended to the query string or as specified by the API.

Cross-Origin Resource Sharing

JSONP is a trick but it works well. CORS is a more recent strategy that is supported by nearly all browsers that have been released in the past five years. So probably CORS is the way to go, and you would only need to add JSONP if you have specific requirements to support all possible browsers that may happen to reach your site.

The idea behind CORS is pretty simple: it is to let the remote site decide whether it wants to share data or not. Therefore, CORS is all about setting up a handshake between the calling browser and the remote site. If the browser figures out that the site is available to share data, then any received data is passed back to the calling JavaScript function.

A browser that supports CORS adds a request header called Origin and sets it to the name of the requesting site before performing the request. As it turns out, the request is always placed, but results are shared with the calling JavaScript context only if the browser receives permission to share from the remote endpoint. So when the response from the cross-domain site is received, the browser checks for the Access-Control-Allow-Origin response header. If no such header is found, the SOP is enforced and any response is ignored. If the value of the header is * then the JavaScript call is completed successfully as if it were a local call and the downloaded response is made available within the DOM of the page. If the header contains any value different from * then the value is matched to the current origin URL. If a match is found, the request completes successfully, otherwise the response received is ignored.

A CORS-enabled HTTP endpoint is only requested to add the header if it agrees to have its response passed to callers from outside the domain. An ASP.NET MVC JSON endpoint achieves the goal with one line of code as below:

The response header allows to control the sites from which it is acceptable to receive requests. Here’s a more sophisticated way to add the CORS response header.

If the request is local (i.e., you’re testing the site) or if it comes from a known origin, the endpoint adds the CORS header set to the name of the requesting site. The function IsKnownOrigin in the code snippet is a placeholder for any code you may have to check the origin URL against a collection of authorized callers.

This approach offers the greatest flexibility but it requires changes to existing code to be enabled. It might be interesting to know that CORS control can also be enabled declaratively by adding some configuration to the web.config file.

Unfortunately, the customHeaders section doesn’t support multiple sites. All you can achieve with the declarative approach is to enable all cross-domain callers or just one. If you run your own code to check CORS access, then you can also add further code to IsKnownOrigin so as to check for the HTTP verb being used. However, CORS browsers also support automatic HTTP verb check via another header: Access-Control-Allow-Methods. In this case, multiple HTTP verbs can be specified separated by the comma.

The Plain Old Proxy Approach

CORS and JSONP are solutions to place calls to a remote, cross-domain endpoint from within the DOM of a web page, therefore via JavaScript. No such limitations exists if you perform a server-to-server call. If for some reasons neither JSONP nor CORS work for you, you still have the option of creating a façade around the cross-domain endpoints and proxy JavaScript calls to it. In this way, any JavaScript Ajax calls are directed at your own site (hence, the Same Origin Policy is met) but the façade will perform server-side calls to any URLs.

As you can see, this approach requires an extra HTTP call but, on the other hand, it works in all possible cases and gives you the power of deciding in full freedom the best caching strategy for the downloaded content.

Calling a cross-domain URL from JavaScript is no longer a problem today, but overall CORS seems to be the more reliable and flexible way to go if you can afford it.