In this world of the web, we have seen various common attacks like XSS, Clickjacking, Session Hijacking, etc. Various HTTP headers are introduced to defend against these attacks in a simple and easy fashion. In this series of articles, we will see various headers available to protect against common web attacks and we will also see a practical approach of how to implement them in a simple PHP based application.

The focus of this series is to give developers a practical touch of how these common attacks can be prevented just by using some HTTP headers. We will setup a vulnerable application to understand these headers in detail.

Setting up the lab:

You can download the code snippets and database file used in this application here:

You can set up this PHP-MYSQL application in XAMPP or WAMP or LAMP or MAMP, depending upon your machine.

In my case, I am using a Mac machine and thus using MAMP, and I kept all the files in a folder called “sample” inside my root directory.

Application functionality:

After setting up the sample application, launch the home page as shown below.

http://localhost/sample/index.php

As we can see in the above figure, this application has got a very simple login page where the user can enter his credentials. It has got basic server side validations as explained below.

The user input fields cannot be empty. This is done using PHP’s empty() function. So, if a user doesn’t enter anything and clicks login, it throws a message as shown below.

If the user enters wrong credentials, it throws a message as shown below. This is done after performing a check against user database.

If the user enters correct username and password, it goes ahead and shows the home page for the user logged in.

This is done using the MySQLi prepared statement as shown below.

$stmt = $mysqli->prepare("select * from admin where username=? and password=?");

 $stmt->bind_param("ss",$username,$password);

 $stmt->execute();

 username: admin
password: 1q2w3e4r5t

Note: Please keep in mind that the given password is stored as SHA1 hash in this sample database. This is a common password and this SHA1 hash can be easily be cracked using some online tools.

After logging in, a session is created for the user, and there is a simple form which is vulnerable to XSS.

Now, let us fire up BurpSuite and just keep a note of the default headers that are set when we login to this application. This looks as shown below.

HTTP/1.1 200 OK

 Date: Sun, 12 Apr 2015 13:59:23 GMT

 Server: Apache/2.2.29 (Unix) mod_fastcgi/2.4.6 mod_wsgi/3.4 Python/2.7.8 PHP/5.6.2 mod_ssl/2.2.29 OpenSSL/0.9.8y DAV/2 mod_perl/2.0.8 Perl/v5.20.0

 X-Powered-By: PHP/5.6.2

 Expires: Thu, 19 Nov 1981 08:52:00 GMT

 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

 Pragma: no-cache

 Set-Cookie: PHPSESSID=17807aed72952730fd48c35ac8e58f9c; path=/

 Content-Length: 820

 Keep-Alive: timeout=5, max=100

 Connection: Keep-Alive

 Content-Type: text/html; charset=UTF-8

If you clearly observe the above headers, there are no headers added to provide additional security to this application.

We can also see the search field after logging in, which is accepting user input and echoing back to the user.

Below is the code used to build the page being displayed after login.

<?php
session_start();
session_regenerate_id();
if(!isset($_SESSION['admin_loggedin']))
{
    header('Location: index.php');
}

 if(isset($_GET['search']))
{
    if(!empty($_GET['search']))
    {
        $text = $_GET['search'];
    }
    else
    {
        $text = "No text Entered";
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Admin Home</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>

         <div id="home"><center>
        </br><legend><text id=text><text id="text2">Welcome to Dashboard...</text></br></br> You are logged in as: <?php echo $_SESSION['admin_loggedin']; ?> <a href="logout.php">[logout]</a></text></legend></br>
        <form action="" method="GET">
            <div id="search">
            <text id="text">Search Values</text><input type="text" name="search" id="textbox"></br></br>

             <input type="submit" value="Search" name="Search" id="but"/>

             <div id="error"><text id="text2">You Entered:</text><?php echo $text; ?></div>

             </div>
        </form></center>
    </div>

     </body>
</html>

Clickjacking prevention using X-Frame-Options header:

The first concept that we will discuss is Clickjacking mitigation using X-Frame-Options.

Ethical Hacking Training – Resources (InfoSec)

How does it work?

Usually, an attacker loads a vulnerable page into an iframe to perform clickjacking attacks.

In our case, we are going to load the user dashboard page into an iframe as shown below. This page appears after successful login.

http://localhost/sample/home.php

<!DOCTYPE html>
<html>
    <head>
        <title>iframe</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <iframe src="http://localhost/sample/home.php"></iframe>
    </body>
</html>

I saved this page as iframe.html on the same server. When we load this in a browser, the above URL will be loaded in an iframe as shown below.

Though there are multiple ways to prevent this, we are going to discuss the X-Frame-Options header to keep the content of this article inline with the title.

The X-Frame-Options header can be used with the following three values:

DENY: Denies any resource from framing the target.

SAMEORIGIN: Allows only resources that are part of the Same Origin Policy to frame the protected resource.

ALLOW-FROM: Allows a single serialized-origin to frame the protected resource. This works only with Internet Explorer and Firefox.

We will discuss each of these options in detail.

X-Frame-Options: DENY

Let us start with “X-Frame-Options: DENY”.

Open up your home.php file and add the following line.

header(“X-Frame-Options: DENY”);

Now the modified code should look as shown below.

<?php
session_start();
session_regenerate_id();

 header("X-Frame-Options: DENY");

 if(!isset($_SESSION['admin_loggedin']))
{
    header('Location: index.php');
}

 if(isset($_GET['search']))
{
    if(!empty($_GET['search']))
    {
        $text = $_GET['search'];
    }
    else
    {
        $text = "No text Entered";
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Admin Home</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>

         <div id="home"><center>
        </br><legend><text id=text><text id="text2">Welcome to Dashboard...</text></br></br> You are logged in as: <?php echo $_SESSION['admin_loggedin']; ?> <a href="logout.php">[logout]</a></text></legend></br>
        <form action="" method="GET">
            <div id="search">
            <text id="text">Search Values</text><input type="text" name="search" id="textbox"></br></br>

             <input type="submit" value="Search" name="Search" id="but"/>

             <div id="error"><text id="text2">You Entered:</text><?php echo $text; ?></div>

             </div>
        </form></center>
    </div>

     </body>
</html>

Logout from the application and re-login to observe the HTTP headers now.

Below are the HTTP headers from the server after adding X-Frame-options header with the value DENY:

HTTP/1.1 200 OK

 Date: Sun, 12 Apr 2015 14:14:51 GMT

 Server: Apache/2.2.29 (Unix) mod_fastcgi/2.4.6 mod_wsgi/3.4 Python/2.7.8 PHP/5.6.2 mod_ssl/2.2.29 OpenSSL/0.9.8y DAV/2 mod_perl/2.0.8 Perl/v5.20.0

 X-Powered-By: PHP/5.6.2

 Expires: Thu, 19 Nov 1981 08:52:00 GMT

 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

 Pragma: no-cache

 Set-Cookie: PHPSESSID=9190740c224f78bb78998ff40e5247f3; path=/

 X-Frame-Options: DENY

 Content-Length: 820

 Keep-Alive: timeout=5, max=100

 Connection: Keep-Alive

 Content-Type: text/html; charset=UTF-8

If you notice, there is an extra header added in the response from the server.

If we reload the iframe now, the URL will not be loaded inside the iframe. This looks as shown below.

Let us see the reason behind this by navigating to Chrome’s developer tools using the following path.

Customize and Control Google Chrome -> More Tools -> Developer Tools\

As we can see in the above figure, this is because of the header we set in the server response.

We can check the same in Firefox by using the Web Developer Extension as shown below.

If we load the iframe.html page in Firefox, below is the error being displayed in the console.

X-Frame-Options: SAMEORIGIN

There may be scenarios where framing of this URL is required for this application. In such cases, we can allow framing from the same origin and prevent it from cross origin requests using the value “SAMEORIGIN” with X-Frame-Options header.\

Open up your home.php file and add the following line.

header(“X-Frame-Options: sameorigin”);\

Now the modified code should look as shown below.

<?php
session_start();
session_regenerate_id();

 header("X-Frame-Options: sameorigin");

 if(!isset($_SESSION['admin_loggedin']))
{
    header('Location: index.php');
}
if(isset($_GET['search']))
{
    if(!empty($_GET['search']))
    {
        $text = $_GET['search'];
    }
    else
    {
        $text = "No text Entered";
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Admin Home</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>

         <div id="home"><center>
        </br><legend><text id=text><text id="text2">Welcome to Dashboard...</text></br></br> You are logged in as: <?php echo $_SESSION['admin_loggedin']; ?> <a href="logout.php">[logout]</a></text></legend></br>
        <form action="" method="GET">
            <div id="search">
            <text id="text">Search Values</text><input type="text" name="search" id="textbox"></br></br>

             <input type="submit" value="Search" name="Search" id="but"/>

             <div id="error"><text id="text2">You Entered:</text><?php echo $text; ?></div>

             </div>
        </form></center>
    </div>

     </body>
</html>

Logout from the application and re-login to observe the HTTP headers now.

Below are the HTTP Headers from the server after adding X-Frame-options header with the value sameorigin:

HTTP/1.1 200 OK

 Date: Sun, 12 Apr 2015 14:34:52 GMT

 Server: Apache/2.2.29 (Unix) mod_fastcgi/2.4.6 mod_wsgi/3.4 Python/2.7.8 PHP/5.6.2 mod_ssl/2.2.29 OpenSSL/0.9.8y DAV/2 mod_perl/2.0.8 Perl/v5.20.0

 X-Powered-By: PHP/5.6.2

 Expires: Thu, 19 Nov 1981 08:52:00 GMT

 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

 Pragma: no-cache

 Set-Cookie: PHPSESSID=5f3d66b05f57d67c3c14158621dbba9e; path=/

 X-Frame-Options: sameorigin

 Content-Length: 820

 Keep-Alive: timeout=5, max=100

 Connection: Keep-Alive

 Content-Type: text/html; charset=UTF-8

Now, let us see how it works with different origins.

First let us load the same iframe.html, which is hosted on the same server.

As we can see in the figure below, we are able to load the page in the iframe without any problem.

Now, I launched Kali Linux using Virtual Box and loaded this URL(http://localhost/sample/home.php)and placed the file on the server, which is a different origin for our current application.

Below is the code snippet used on the Kali Linux machine to create iframe.html.

When we launch this iframe.html file, it will not load due to the cross origin restriction by the server.

We can see that in the error console of iceweasel browser in Kali Linux as shown below.

The error clearly shows that the server does not allow cross-origin framing.

X-Frame-Options: ALLOW-FROM http://www.site.com

X-Frame-Options: ALLOW_FROM option allows a single serialized-origin to frame the target resource. This works only with Internet Explorer and Firefox.

Let us see how this works.

First, open up your home.php file and add the following line.

header(“X-Frame-Options: ALLOW-FROM http://localhost”);

Now the modified code should look as shown below.

<?php
session_start();
session_regenerate_id();
header("X-Frame-Options: ALLOW-FROM http://localhost");
if(!isset($_SESSION['admin_loggedin']))
{
    header('Location: index.php');
}
if(isset($_GET['search']))
{
    if(!empty($_GET['search']))
    {
        $text = $_GET['search'];
    }
    else
    {
        $text = "No text Entered";
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Admin Home</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>

         <div id="home"><center>
        </br><legend><text id=text><text id="text2">Welcome to Dashboard...</text></br></br> You are logged in as: <?php echo $_SESSION['admin_loggedin']; ?> <a href="logout.php">[logout]</a></text></legend></br>
        <form action="" method="GET">
            <div id="search">
            <text id="text">Search Values</text><input type="text" name="search" id="textbox"></br></br>

             <input type="submit" value="Search" name="Search" id="but"/>

             <div id="error"><text id="text2">You Entered:</text><?php echo $text; ?></div>

             </div>
        </form></center>
    </div>

     </body>
</html>

Let us logout from the application and re-login to check if the header is added.

HTTP/1.1 200 OK

 Date: Mon, 13 Apr 2015 02:18:49 GMT

 Server: Apache/2.2.29 (Unix) mod_fastcgi/2.4.6 mod_wsgi/3.4 Python/2.7.8 PHP/5.6.2 mod_ssl/2.2.29 OpenSSL/0.9.8y DAV/2 mod_perl/2.0.8 Perl/v5.20.0

 X-Powered-By: PHP/5.6.2

 Expires: Thu, 19 Nov 1981 08:52:00 GMT

 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

 Pragma: no-cache

 Set-Cookie: PHPSESSID=c8a5b9a76982ae38f0dde3f3bf3480f5; path=/

 X-Frame-Options: ALLOW-FROM http://localhost

 Content-Length: 820

 Keep-Alive: timeout=5, max=100

 Connection: Keep-Alive

 Content-Type: text/html; charset=UTF-8

As we can see, the new header is added now. If we now try to load the iframe from the same server, it loads the page without any problem, as shown below.

This is because http://localhost is allowed to load this URL.

Now, let us try to change the header to something else and try reloading it again.

Add the following line in home.php and observe the difference.

header(“X-Frame-Options: ALLOW-FROM http://www.androidpentesting.com”);

The modified code should look as shown below.

<?php
session_start();
session_regenerate_id();
header("X-Frame-Options: ALLOW-FROM http://www.androidpentesting.com");
if(!isset($_SESSION['admin_loggedin']))
{
    header('Location: index.php');
}
if(isset($_GET['search']))
{
    if(!empty($_GET['search']))
    {
        $text = $_GET['search'];
    }
    else
    {
        $text = "No text Entered";
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Admin Home</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>

         <div id="home"><center>
        </br><legend><text id=text><text id="text2">Welcome to Dashboard...</text></br></br> You are logged in as: <?php echo $_SESSION['admin_loggedin']; ?> <a href="logout.php">[logout]</a></text></legend></br>
        <form action="" method="GET">
            <div id="search">
            <text id="text">Search Values</text><input type="text" name="search" id="textbox"></br></br>

             <input type="submit" value="Search" name="Search" id="but"/>

             <div id="error"><text id="text2">You Entered:</text><?php echo $text; ?></div>

             </div>
        </form></center>
    </div>

     </body>
</html>

Following are the headers captured from BurpSuite.

HTTP/1.1 200 OK

 Date: Mon, 13 Apr 2015 02:20:26 GMT

 Server: Apache/2.2.29 (Unix) mod_fastcgi/2.4.6 mod_wsgi/3.4 Python/2.7.8 PHP/5.6.2 mod_ssl/2.2.29 OpenSSL/0.9.8y DAV/2 mod_perl/2.0.8 Perl/v5.20.0

 X-Powered-By: PHP/5.6.2

 Expires: Thu, 19 Nov 1981 08:52:00 GMT

 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

 Pragma: no-cache

 Set-Cookie: PHPSESSID=6a8686e1ab466a6c528d8a49a281c74e; path=/

 X-Frame-Options: ALLOW-FROM http://www.androidpentesting.com

 Content-Length: 820

 Keep-Alive: timeout=5, max=100

 Connection: Keep-Alive

 Content-Type: text/html; charset=UTF-8

If we now refresh our previous link, it will not load the page in an iframe.

If we observe the error console, it shows the following error.

It is obvious that framing by http://localhost is not permitted.

Conclusion

In this article, we have seen the functionality of our vulnerable application and fixed the clickjacking vulnerability using X-Frame-Options header. We have also seen various options available with this header and how they differ from each other. The next article gives coverage of other security headers available.