Hands-on tips for PHP security

PHP SecurityI got asked to review a fairly large piece of PHP code recently and, whoooo boy, was I in for a treat (treat as in clawing my eyes out with a rusty spoon while listening to Nickelback, as interpreted by Dr Zoidberg. In reverse). This magnificent piece of code was an eye opener in many ways and it made me feel a little bit better about myself, to be honest. It employed not only what would be called “bad practice”, but also a lot of plain ol’ stupidity. I know you are just dying for examples and I am not one to deny you the satisfaction. Also note that if this resembles your own code in any way, just leave because we don’t take kindly to you kinds of people, y’hear? And bring your cousin dad too. The beast contained gems such as

$connect_db = mysql_connect("localhost","username","password");
if (!$connect_db) {
    die('Could not connect becaus of ' . mysql_error());
} else {
    mysql_select_db("some_random_db", $connect_db);
    $command="INSERT INTO table_name ('username', /* some other crap */)
    VALUES ('".$_GET['user']."' /* more crappy crap crap */)";
    if (!mysql_query($command,$connect_db)){
        die('Could not add user because ' . mysql_error());
    }
    echo "<h3>Successfuly added user ".$_GET['user']."</h3>";
}




Spelling errors not omitted. No cup of heavenly blessed Kopi Luwak coffee, poured into a cup of golden banana leaves by naked triplets could counter the massive damage done to my corneas or, for that matter, my soul. Or what was left of it. The kicker was that the developer claimed it was OO and showed me the following “controller”. This is paraphrasing a bit, but you’ll get the general idea.

class MySite
{
    var $page;
    function MySite($page) {
        $this->page = $page;
        $content = "page.php?page=".$this->page;
        /*
        This is where page.php returned the entire document
        */
        echo $content;
    }
}
$init =  new MySite($_GET['page']);

Yum.

Needless to say, everything was passed around in the URL. Nothing was escaped as it “would hurt performance”. The e-mail function only checked that there was an @ in the string. You get the picture. Of course, a refactor, and I use the term loosely as it really was a steaming pile of vomit that would need a complete rewrite, was mission critical and the investors didn’t like the idea that the site would be down for maintenance for a while. As it turns out, this started as a hobby project that got the attention of somebody with money and decided that it would make a nice platform for a thriving business.

So this little article will deal with some security related matters such as input filtering. I expect there will be a second part to this as there is a plethora of things to consider, but this’ll do for now.

First off, I’d like to introduce you to a wonderful library without external dependencies called Inspekt. Mr. Ed Finkler of Funkatron built Inspekt upon the now deprecated Zend_Filter_Input, by Chris Shiflett.


[Insert Inspector Gadget joke here]

Inspekt acts as a firewall API between user input and the rest of the application. It takes PHP superglobal arrays, encapsulates their data in an “cage” object, and destroys the original superglobal. Data can then be retrieved from the input data object using a variety of accessor methods that apply filtering, or the data can be checked against validation methods.

Everybody wins! In order to take advantage of this black magic, all you need to do is something like this;

require "Inspekt.php";
$input = Inspekt::makeSuperCage();

Okay, what have we done here? When the library’s been required, we create an Inspekt_Supercage – that’s a single object that contains each input superglobal as an Inspekt_Cage. Inspekt_Cage objects encapsulate arrays of data. The original array is destroyed so you always have to access the data through the Input_Cage object’s methods. Say you need to check if $_POST['important_id'] really is an integer. Note that we are extending the introductory example above.

$input->post->testInt('important_id');

If it is not an integer, it will return false. More on that in a bit. What’s nice about Inspekt is that you can specify the kind of cage to create, i.e. a cage for just $_GET or $_POST, etc. You have the following tools to play with;

Inspekt::makeGetCage(); // Returns an Inspekt_Cage for the $_GET array
Inspekt::makePostCage(); // Returns an Inspekt_Cage for the $_POST array
Inspekt::makeCookieCage(); // Returns an Inspekt_Cage for the $_COOKIE array
Inspekt::makeServerCage(); // Returns an Inspekt_Cage for the $_SERVER array
Inspekt::makeFilesCage() // Returns an Inspekt_Cage for the $_FILES array
Inspekt::makeEnvCage() // Returns an Inspekt_Cage for the $_ENV array

Isn’t that nifty? The Inspekt_Supercage has no less than six public properties, such as get for $_GET, post for $_POST, etc. It correlates to the list just above. I promise. You access them by something like the following (again, extended from our first makeSuperCage example);

$input->get->testEmail('email');

If it’s valid, it will return the value. So, what else do we have in our toolbox? Well, the cage is all good and well, but we need some sort of properties to actually do the filtering or examining values. Let’s start with some tools for testing, shall we? The testers are neat methods to check the value on a given key. They return the value of the key if it’s good and false if it fails.

testAlnum (mixed $key)
// Returns value if every character is alphabetic or a digit

testAlpha (mixed $key)
// Returns value if every character is alphabetic

testBetween (mixed $key, mixed $min, mixed $max, [boolean $inc = TRUE])
// Returns value if it is greater than or equal to $min and less than or equal to $max.
// If $inc is set to FALSE, then the value must be strictly greater than $min and strictly less than $max.

testCcnum (mixed $key, [mixed $type = NULL])
// Returns value if it is a valid credit card number format.
// The optional second argument allows developers to indicate the type.

testDate (mixed $key)
// Returns $value if it is a valid date. The date is required to be in ISO 8601 format.

testDigits (mixed $key)
// Returns value if every character is a digit

testEmail (mixed $key)
// Returns value if it is a valid email format

testFloat (mixed $key)
// Returns value if it is a valid float value

testGreaterThan (mixed $key, [mixed $min = NULL])
// Returns value if it is greater than $min

testHex (mixed $key)
// Returns value if it is a valid hexadecimal format

testHostname (mixed $key, [integer $allow = ISPK_HOST_ALLOW_ALL])
// Returns value if it is a valid hostname

testInt (mixed $key)
// Returns value if it is a valid integer value

testIp (mixed $key)
// Returns value if it is a valid IP format

testLessThan (mixed $key, [mixed $max = NULL])
// Returns value if it is less than $max

testOneOf (mixed $key, [ $allowed = NULL])
// Returns value if it is one of $allowed

testPhone (mixed $key, [ $country = 'US'])
// Returns value if it is a valid phone number format. The optional second argument indicates the country.

testRegex (mixed $key, [mixed $pattern = NULL])
// Returns value if it matches $pattern. Uses preg_match() for the matching.

testUri (string $key)
// Returns value if it is a valid URI as defined in http://www.ietf.org/rfc/rfc2396.txt

testZip (mixed $key)
// Returns value if it is a valid US ZIP

Sexy, isn’t it? And in order to access your values after they’ve been filtered, you may use the following methods;

getAlnum (mixed $key)
// Returns only the alphabetic characters and digits in value.

getAlpha (mixed $key)
// Returns only the alphabetic characters in value.

getDigits (mixed $key)
// Returns only the digits in value. This differs from getInt().

getDir (mixed $key)
// Returns dirname(value).

getInt (mixed $key)
// Returns (int) value.

getPath (mixed $key)
// Returns realpath(value).

getPurifiedHTML (string $key)
// This returns the value of the given key passed through the HTMLPurifer object,
// if it is instantiated with Inspekt_Cage::loadHTMLPurifer

getROT13 (string $key)
// Returns ROT13-encoded version

getValue (string $key)
// Retrieves a value from the cage

getRaw (string $key)
// Returns value, unfiltered.

noPath (mixed $key)
// Returns basename(value).

noTags (mixed $key)
// Returns value with all tags removed.

noTagsOrSpecial (mixed $key)
// Returns value with tags stripped and the chars '"&<> and all ascii chars
// under 32 encoded as html entities

And of course, there is

keyExists (mixed $key)
// Checks if a key exists

It wouldn’t be complete if there where no methods for escaping strings prior to database calls. Well, aren’t we in luck?

escMySQL (mixed $value, [resource $conn = null])
// Escapes the value given with mysql_real_escape_string

escPgSQL (mixed $value, [resource $conn = null])
// Escapes the value given with pg_escape_string

escPgSQLBytea (mixed $value, [resource $conn = null])
// Escapes the value given with pg_escape_bytea

I should point out that you actually can wrap any array in Inspekt_Cage and not just superglobals, making the library a handy dandy tool for all your filtering and testing needs.

$dirty = array( /* imagine some random, unfiltered values here */ );
$johnny_cage = Inspekt_Cage::Factory($dirty);

to test some value in the array, just use

$johnny_cage->testInt('key');

To get even more nitty gritty, you can use static methods for those one off moments.

Inspekt::isAlnum (mixed $value)
Inspekt::isAlpha (mixed $value)
Inspekt::isBetween (mixed $value, mixed $min, mixed $max, [ $inc = TRUE])
Inspekt::isCcnum (mixed $value, [mixed $type = NULL])
Inspekt::isDate (mixed $value)
Inspekt::isDigits (mixed $value)
Inspekt::isEmail (string $value)
Inspekt::isFloat (string $value)
Inspekt::isGreaterThan (mixed $value, mixed $min)
Inspekt::isHex (mixed $value)
Inspekt::isHostname (mixed $value, [integer $allow = ISPK_HOST_ALLOW_ALL])
Inspekt::isInt (mixed $value)
Inspekt::isIp (mixed $value)
Inspekt::isLessThan (mixed $value, mixed $max)
Inspekt::isOneOf (mixed $value, [ $allowed = NULL])
Inspekt::isPhone (mixed $value, [ $country = 'US'])
Inspekt::isRegex (mixed $value, [mixed $pattern = NULL])
Inspekt::isUri (string $value, [integer $mode = ISPK_URI_ALLOW_COMMON])
Inspekt::isZip (mixed $value)
Inspekt::getAlnum (mixed $value)
Inspekt::getAlpha (mixed $value)
Inspekt::getDigits (mixed $value)
Inspekt::getDir (mixed $value)
Inspekt::getInt (mixed $value)
Inspekt::getPath (mixed $value)
Inspekt::noPath (mixed $value)
Inspekt::noTags (mixed $value)

Built in goodies

If you feel that using a library isn’t cool enough, or if I haven’t made the benefits clear, there are some things you ca do for filtering your values. I’m talking about the often overlooked PHP Filter functions. These are available if you are using at least PHP 5.2. Before continuing, I want to point you to the types of filters available. They’re validation filters, sanitizing filters, miscellaneous filters and, well, the predefined constants. First off is filter_var.




filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

filter_var does what you think it does – it filters a variable with the specified filter. Go tautology! the $filter is specified in the list in the previous paragraph. The $options are an array of options or bitwise disjunction of flags. The function returns the filtered data or false if it fails. A quick example of e-mail validation could be

$is_it_okay = filter_var("some_dude\X00= _@ne@example..com", FILTER_VALIDATE_EMAIL);

Then there’s filter_var_array, which allows you to check an array of values.

filter_var_array ( array $data [, mixed $definition ] )

An example could be (borrowed from this place)

$data = array('<strong>bold</strong>', '<script type="text/javascript">// <![CDATA[
  javascript
// ]]></script>', 'P*}i@893746%%%p*.i.*}}|.dw');
$myinputs = filter_var_array($data,FILTER_SANITIZE_STRING);
var_dump($myinputs);

//OUTPUT:
//formarray(3) { [0]=> string(4) "bold" [1]=> string(10) "javascript"
[2]=> string(26) "P*}i@893746%%%p*.i.*}}|.dw" }

Then we have filter_input which gets an external variable by name and optionally filters it.

filter_input ( int $type , string $variable_name [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

$type may be either INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER or INPUT_ENV ( INPUT_SESSION and INPUT_REQUEST are not implemented yet). Returns the requested variable on success, false if not or null if the $variable_name is not set (unless you use the flag FILTER_NULL_ON_FAILURE – in that case it returns false if the variable is not set and null if it fails). Say you want to sanitize $_GET['search'] for special characters. Try something like

$search = filter_input(INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS);

There is also filter_input_array, which does what you think it does – It filters an array instead of just a single variable.

$example_filters = array (
    "url" => FILTER_VALIDATE_URL,
    "ip" => FILTER_VALIDATE_IP,
    "temeraturep" => array(
        "filter"=>FILTER_VALIDATE_INT,
        "options"=>array(
            "min_range"=>1,
            "max_range"=>666
         )
    )
);

$sweetness = filter_input_array(INPUT_POST, $example_filters);

A quickie is filter_has_var, which checks if a variable of a specified type exists or not.

filter_has_var ( int $type , string $variable_name )

$type can be either INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER or INPUT_ENV.


CRSFXSSFRSFCSFCSS X XSS SS XX C FFCCCE

A note on CRSF and XSS as well, while we’re at it. Inspekt allows you to run your data through the getPurifiedHTML method which basically runs it through HTMLPurifier and should provide a reasonable amount of XSS protection. If you have any tips on other XSS prevention projects or functions, please let me know. I stumbled upon a related project, csrf-magic. It uses PHP’s output buffering to “dynamically rewrite forms and scripts” and also check tokens in POST requests. You call it with

include_once 'csrf-magic.php';

It will even dynamically rewrite AJAX requests that uses XMLHttpRequest, if you want it to. Pretty sweet. It’s supposed to play along quite nicely with jQuery, MooTools, Prototype, Ext, Dojo, Yahoo UI Library, script.aculo.us and Prototype. There is also an option to set a configuration in the PHP script. Check out the following (borrowed form the bundled readme);

// This gets called if a csrf check fails
function my_csrf_callback() {
    echo "You're doing bad things young man!";
}

function csrf_startup() {

// While csrf-magic has a handy little heuristic for determining whether
// or not the content in the buffer is HTML or not, you should really
// give it a nudge and turn rewriting *off* when the content is
// not HTML. Implementation details will vary.
if (isset($_POST['ajax'])) csrf_conf('rewrite', false);

// This is a secret value that must be set in order to enable username
// and IP based checks. Don't show this to anyone. A secret id will
// automatically be generated for you if the directory csrf-magic.php
// is placed in is writable.
csrf_conf('secret', 'ABCDEFG123456');

// This enables JavaScript rewriting and will ensure your AJAX calls
// don't stop working.
csrf_conf('rewrite-js', '/csrf-magic.js');

// This makes csrf-magic call my_csrf_callback() before exiting when
// there is a bad csrf token. This lets me customize the error page.
csrf_conf('callback', 'my_csrf_callback');

// While this is enabled by default to boost backwards compatibility,
// for security purposes it should ideally be off. Some users can be
// NATted or have dialup addresses which rotate frequently. Cookies
// are much more reliable.
csrf_conf('allow-ip', false);

}

// Finally, include the library
include_once '/path/to/csrf-magic.php';




If you would like to roll your own CSRF solution, a simple one can be scripted on a coffee break. When dealing with forms, add a random token to your $_SESSION after calling session_start() such as;

$_SESSION['my_token'] = md5(uniqid(rand(), TRUE));

And then in the form itself, add a hidden input like so;

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

After submit, you can check if the token is valid with something simple as;

if ($_POST['my_token'] == $_SESSION['my_token']){
    /* Fun stuff */
}

You may ask, why don’t we limit the amount of time a token is valid? Yes, well, let’s do that then.

// Add token and time
$_SESSION['my_token'] = md5(uniqid(rand(), TRUE));
$_SESSION['my_token_time'] = time();

// Assume we now submitted the form, yadda yadda

$are_we_screwed = time() - $_SESSION['my_token_time'];

if ($_POST['my_token'] == $_SESSION['my_token']){
    /* It's all good! Or is it? */
    if ( $are_we_screwed < 60 ) {
        /* Less than a minute to play with */
    } else {
        /* Fail */
    }
}
Will code HTML for foodThere you have it. Some quick tips and/or things to think about. I’m not sure how I can end this little tidbit, so I’ll just paste a picture to keep you busy. Mm’kay?

Update: Reddit user pashj gave me a tip on adding a link to XSS examples so you can test you own project for, well, attack vectors. A classic cheat sheet full of examples is this one over at ha.ckers.org. Try it out! Let me know how it goes.

Related posts:

  1. A Popurls Clone with PHP, jQuery, Awesomeness Good people of the Internets, I know a good deal...
  • http://abcphp.com/story/3173/ abcphp.com

    Hands-on tips for PHP security | profeshunl newbie…

    I got asked to review a fairly large piece of PHP code recently and, whoooo boy, was I in for a treat (treat as in clawing my eyes out with a rusty spoon while listening to Nickelback, as interpreted by Dr Zoidberg. In reverse). No cup of heavenly bles…

  • http://www.ubervu.com/conversations/pronewb.com/hands-on-tips-for-php-security uberVU – social comments

    Social comments and analytics for this post…

    This post was mentioned on Twitter by pronewb: PHP Security, some hands-on tips http://bit.ly/ddqJdj #PHP #security…

  • http://webbyscripts.com/blog/2010/03/hands-on-tips-for-php-security-profeshunl-newbie/ Webby Scripts Hands-on tips for PHP security | profeshunl newbie

    [...] the original here: Hands-on tips for PHP security | profeshunl newbie [...]

  • http://www.hogscripts.com php scripts

     always a pleasure stopping by your site thanks

Page 1 of 11
  • What is this?

    My name is William and I'm a 30 year old developer/designer from Stockholm, Sweden. I have a love/hate relationship with PHP, I'm slightly aroused by jQuery and if I had the Adobe Flash IDE as a friend on Facebook, I'd label it as "it's complicated". This is my twelfth year as a freelance monkey. I prefer the term mercenary, but someone said it had a negative ring to it. Whatever. Oh, and I'm a Mac guy who loves his BacBook Pro in a somewhat unhealthy way.


    The font used for headings is Geometry Soft Pro as found on dafont.com.