AJAX, jQuery & how to bypass the same-origin policy

AJAX und Cross-DomainToday’s post is intended to give a brief introduction to the topic of AJAX with jQuery, as well as to cover the topic of cross domain (policy) and to show how you can use small tricks to send AJAX requests across multiple domains. Before we start, however, we will give you a quick introduction to the topic. If you are already familiar with the topic and are only interested in the AJAX cross-domain workaround, you can safely skip the first part of the article.

What is AJAX and how does it work in jQuery?

AJAX is an abbreviation and stands for “Asynchronous JavaScript And XML”. Contrary to the opinion of some people on the Internet, it is not a programming language, but rather a pattern, i.e. a way of programming something. By means of AJAX web content can be retrieved via Javascript, i.e. client-side in the browser. I say contents consciously, because contrary to the name “… And XML” not only XML files can be loaded, but also JSON, plaintext and other conceivable contents.

synchrounous requests“And what do I need AJAX for?”, you may ask now. In the “early days” of the internet (and on badly programmed websites still today), a web page is reloaded every time content is to be fetched from the server.

As a simple example, let’s take a small login form. (First of all the following hint: The following login mechanism is only for illustration. Secure logins are programmed differently! More about this topic can be found in an article I wrote down here).

Now we could write a PHP script, which looks like this:

<html>
<head>
    <title>Login</title>
</head>
<body>
    <form method="post" action="">
        <input type="password" id="passwd" name="pass">
        <input type="submit">
    </form>
<?php
if (isset($_POST["pass"])){
     
    $password = $_POST["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>
</body>
</html>

(You can find a demo of the script here: login.php-Demo).

When the script is loaded, it shows a login form and checks in the PHP section of the script if the post variable “pass” is set. The first time the script is called, it is not set, so nothing else happens. If you enter a password and press “Send”, the script sends the form input to itself. This time the post variable is set by the form. The script now evaluates the sent password and checks if it is “secure”.

Send content without reloading the page

Javascript AsyncThe script shown above works, but causes the page to be completely reloaded every time. This may work for such a simple example, but for a real website this is more than annoying and time consuming. But this problem can be avoided by using AJAX.

Instead of sending the whole form to itself and reloading the page, we can use AJAX to pass only the password to the server and evaluate the check result. To do this, we first separate the password logic from the form and outsource it to a second script called login-check.php:

<?php
if (isset($_GET["pass"])){
     
    $password = $_GET["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>

For simplicity, we have changed the mode from Post to Get. Now the script can be called with the password and returns the status. You can find the demo here: login-check.php. A call could look like this:

https://code-bude.net/downloads/ajax-crossdomain/login-check.php?pass=secure

We now have a separate script to check the password input. We now want to call this in our actual login form via AJAX. To simplify the call we use the jQuery library and here especially its “get”-function.

<html>
<head>
    <title>Login</title>
    <script src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
    <input type="password" id="passwd" name="pass">
    <input type="button" id="btn" value="Check">
    <span id="info"><span>
 
    <script type="text/javascript">      
        $('#btn').click(function(){
            var passValue = $('#passwd').val();        
            $.get('login-check.php', { pass: passValue }, function(data){          
                $('#info').html(data);         
            });        
        });
    </script>
</body>
</html>

On the login form, we first added the reference to the jQuery library. Further we changed the button from type “submit” to “button”, because we don’t want to submit a form in the classical sense anymore. Finally, we removed the PHP part and replaced it with javascript. The script attaches itself via “click” to the click event of the “check” button. If it is pressed, the value of the password field is stored in the variable “passValue” via “val()”. Finally, the value is sent to our login-check.php script via the “get” function. The response of the script is caught in the data variable and output via “html” function in the span tag.
A demo of the AJAX form can be found here: login-ajax.html.

AJAX Debugging Developer ToolsIf you click on the check button, the password is sent to the login check script in the background and the answer is displayed without reloading the entire page. If you want to know what happens in the background, just press the F12 key while in the web browser before clicking on “Check”. This opens the developer tools, where you can watch the request being sent.

The screenshot shows how the password is sent to the second script. The Get-parameter is marked in yellow. In the response tab (also marked yellow) you can see the response of the second script.

The vexed topic of cross-domain and the SOP

Let us now turn to the topic of cross-domain AJAX and thus also to the so-called Same-Origin-Policy (=SOP). The Same-Origin-Policy is a security concept that is implemented by all modern browsers and ensures that only pages of the same origin can be called via AJAX. Among other things, this is intended to assure the user that all the content he sees really comes from the page whose website he has just opened. Our example above is not affected by this, as our Ajax call has a relative path (“login-check.php”) and thus points to the same server/domain.

However, if we now extend our example as follows, the whole “drama” becomes apparent. Let’s assume that we have a central password management server that is to be used by several web applications. On this server is the login-check.php script that checks the passwords. For our example this is the server which is reachable under raffaelherrmann.de. The script can be reached at the following address:

https://raffaelherrmann.de/demos/login-check.php?pass=secure

Now we adjust our login script in line 14 and exchange the address:

//old
//$.get('login-check.php', { pass: passValue }, function(data){
 
//new
$.get('https://raffaelherrmann.de/demos/login-check.php', { pass: passValue }, function(data){ 

When calling our login script or clicking on the button, we will notice that nothing happens. A look into the developer tools of the browser shows us that the same-origin policy has struck. After all, our login script with “code-bude.net” is on a different domain than the check script on “raffaelherrmann.de”.
A demo can be seen here: login-ajax-sop.html

As the following screenshots show, the request with an Http status of 200 was successful in itself, but the request was still blocked by the browser. The error console shows the Same-Origin-Policy error message.

AJAX Same-Origin-Policy Example-1  AJAX Same-Origin-Policy Example-2

Finally, let’s take a look at how to bypass the SOP and what ways there are to use AJAX cross-domain (i.e. across multiple domains).

AJAX Cross-Domain with CORS

javascript corsTo be able to work across multiple domains after all, there are at least two variants.

Variant 1, which is more recommendable, is to set a specific http header. Because as we could see in the screenshots, the request takes place at the raffaelherrmann.de-server/domain, although the SOP has pulled. This happens for the following reason:

If the server requested via AJAX answers with a specific header, the retrieval is allowed and the same-origin policy does not intervene. This procedure is called “Cross-Origin Resource Sharing” (short: CORS) and is essentially based on the so-called “Access-Control-Allow-Origin”-header. In order to get our script up and running, we have to add this header to the login-check.php script.

<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
 
if (isset($_GET["pass"])){
     
    $password = $_GET["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>

In addition, we have used a second header, the “Access-Control-Allow-Methods” header, which specifies for which methods the “lock is lifted”. The adapted script is now here: login-check-cors.php

If we now adjust the target in our login page to the new version of the login script, the login will also succeed.

//old
//$.get('https://raffaelherrmann.de/demos/login-check.php', { pass: passValue }, function(data){   
 
//new
$.get('https://raffaelherrmann.de/demos/login-check-cors.php', { pass: passValue }, function(data){

You can find the working demo here: login-ajax-cors.html

AJAX cross-domain with “PHP proxy

proxy-scriptHowever, the solution just shown using CORS headers has one major drawback: you need access to the script to be called. In some cases, especially in the business environment, this is not possible at all or only with a great deal of bureaucratic effort. In these cases there is still the possibility of a “proxy”. (I write this deliberately in quotation marks, because the following solution does not mean a full-fledged proxy server, but encapsulates connections in the sense of a proxy server).

So if you need to access a file on another server, but the same-origin policy forbids it, you still have the option to place a script on your own server, which retrieves the data from the foreign server or sends it to the foreign server. This circumvents the SOP, because the AJAX script calls the “proxy script” on the own server. The called proxy script runs on the server side and is therefore allowed to load any resources, even those of the foreign server.

An example of such a proxy script that fits our example today could look like this:

<?php
if (isset($_GET["pass"])){ 
    $password = $_GET["pass"];
    echo file_get_contents("http://raffaelherrmann.de/demos/login-check.php?pass=".$password);
}
?>

A demo of the proxy script can be found here: proxy.php

The script is quickly explained. It takes the parameter “pass” and calls the remote login-check.php script with this parameter via the function “file_get_contents”. In this way we have elegantly circumvented the same-origin policy problem.

One final note: The proxy script just shown will of course only work for our example today and will need to be adapted for use in other scenarios. Further, one should be aware that this second variant may not always be met with favor, as it circumvents the SOP. After all, one of the purposes of the SOP is also that data should not be loaded from foreign servers without being asked. With great power comes great responsibility. So please use the knowledge with this reminder in mind!

3 Comments

  1. Having read your posts. I believed you have given your readers valuable information. Feel free to visit my website QU5 and I hope you get additional insights about Airport Transfer as I did upon stumbling across your site.

  2. Axel Kappatschsays:

    Excellent article, thank you very much.
    But frankly, the whole AJAX SOP seems not very “thought through” (‘durchdacht’, in German).
    First, because in some cases it can easily be jeopardized by file_get_contents().
    Second, the CORS solution – asking another website to put headers – is in nearly no case practicable.
    Third, not all data requests are security sensitive. So the inverse of SOP should be implemented: a site who does NOT want to provide its data should not honor AJAX requests.
    Fourth, in a technical environment, web servers are firmware in devices with (localhost-)LAN access and no way (and no need) to modify them.
    Fifth, to go from server side (PHP) to client side (JS) only to be obliged to go back to PHP, is – as the Germans would say, “von hinten durch die Brust ins Auge” – i.e. technically very ugly.
    I bet they will come up with more elegant solution in the near future.

  3. STsays:

    Thank you! This is exactly the level of detail I needed.

Leave a comment

Please be polite. We appreciate that. Your email address will not be published and required fields are marked