Monday, February 13, 2012

PHP Security


Security is pretty hard, and it's always evolving, this is why I do not claim that if you follow everything that I say here your website will be 100% protected.
Theses infos are from my personal experiences, and how I manage to secured my websites over the time. I only hope I won't say something false.

Remember, when we are talking about security, one tutorial is good, 2 tutorial is better, 10 tutorials is the best.
So before doing anything here, please, look for other sources, first to be sure you understand, second just in case I'm wrong on a point.

Sql injection

Sql injection isn't new, but their a lot of website out there that are still vulnerable against Sql injection.
And by the way, i'm not saying that everything that I do is Sql injection free, but I try to be very carefull for this.

- So what exactly Sql injection can do?
Sql injection can do pretty much anything. It can be used to login to a website without the username/password, it can retreive data directly from your database, or modify data that someone isn't supposed to modify, or it can even delete data/entire database.

I hope I scared you a little bit, and you all will take Sql injection seriously!

Open Sql Query Browser, select a table (for this exemple I will choose the table called "User") and try this
Code:
select * from User where username = 'Bob' -- and password = '123'
Even if Bob's password isn't 123, bob will show, since the password part is now only a comment and won't be executed.
And please, I saw a lot of tutorials that was simply saying to remove char like "--" and everything will be correct. Well this is stupid, what if the Bob password contain "--"? And this is even more stupid since it don't help against all type of Sql injection try this one:
Code:
select * from User where username = 'a' = 'b'
Well this dosen't make any sens is it?
It's a little bit complex, but 'a' = 'b' = 'c' will always return true, so even if you remove all the "--" char, you will still be subject to sql injection!

- What does it have to do with php?
Well, imagine a login page, where a user enter the username "Bob --", if you don't check for sql injection, this will do pretty much the same thing as my first exemple! And the user will be able to connect as bob.

- So how to prevent Sql injection?
First step is to sanitize your data.
If you are supposed to receive a checkbox answer, but you received a string, their something wrong.
If you are supposed to receive a string with 30char max, but you received something with 200char, you should recheck your data.
So, you need to always validate if you receive the good data for everything, always sanitize your variables ($_POST, $_GET and $_COOKIES, yes even cookies)

Second, you should always know where your data come from.
In this forum, and in a lot of other forums, I seen a lot of person using the variable $_REQUEST, even if a lot of programmer will say there is no problem with using the variable $_REQUEST, I will not suggest it.
Since $_REQUEST can come either from the post, get or cookies (not always cookies depends of your version of php and config) you can't always be sure if the variable isn't overrited by a other variables.
The arguments when someone is telling me that $_REQUEST is as good as post or get is that neither of them are secure and you have simply to sanitize all of them anyway.
That is true, but imagine a that you did a simple mistake and use the same variable in the post and in the get, one of them will be overrited, and it can take some time to understand the problem.

But to sanitize or knowing where the data come from will only help you a little but, it's not all.
First of all, please, please, stop using mysql extension, start using mysqli
mysqli or even better, use PDO (PHP: PDO - Manual).

The mysql extension is old (very old), slow, and not OOP.
Mysqli and PDO is more secure and are OOP

So if you use mysqli you simply need to sanitize all your data with mysqli_real_escape_string
Code:
$mysqli = new mysqli("localhost", "root", "password", "db1");
$username = $mysqli->real_escape_string($username);
$mysqli->query("select * from User where username = '$username'");
$mysqli->close();
If you are using PDO, you either prepare or quote all your data
Code:
$dbh = new PDO("mysql:host=$hostname;dbname=$db", $username, $password);
$sth = $dbh->prepare("SELECT * FROM users WHERE id = :id");
$sth->execute(array(':id' => $id));
while ($result = $sth->fetch())
{
//Code here
}
And with this, you should be protected againse sql injection... By the way, I said you SHOULD, not will, so do some backup, and keep a eye on your user.

Html injection

Html injection is a little less problematic than sql injection, but still you should validate against this.
So imagine that you write your own forum, where a user can choose his username. The user bob decide to add a little special code on his username, and his username is "bob</div></div>"
So now everything Bob post something on the forum, there will be 2 end of div that you don't want, and this may break the design, and render everything hard to read.
This is pretty easy to resolv, but you have to think of it.
Insted of doing something like:
Code:
echo($username);
you should do
Code:
echo(htmlentities($username, ENT_NOQUOTES, 'UTF-8'));
That way the displayed name of bob will be "bob</div></div>" and the 2 end of div will be considerated as normal text and not as html text since both char "<" and ">" will be transformed in their hexadecimal format.
To prevent Html injection isn't hard, but you have to think each time you want to display an user data, you have to sanitize it!
 
Xss

Xss stand for Cross-site-scripting, is a little bit like html injection, but more serious.
Imagine now that bob decided to have a username like this 'bob<script language="javascript">alert("Hello World!");</script>
Everytime his name will be on the forum, a alert will popup. This ain't too bad, but imagine that this is not an alert, but a redirection, the user will be redirected on a other page without his knowledge, he may than be asked some personal info, and the user thinking he's still on your trusted website will enter them. Or this could be a redirection to download a file that contain a virus, or even simply a ajax call to a other server sending his personal infos. And don't think that because of the same domain policy,
you are safe, this is pretty easy to by pass. The user will never know, and you as an administrator of the site may take some time to find this (If you ever find it).

So how can we prevent xss?
Xss can be a little more tricky to prevent, but first step is (you guess it) sanitize your data!
A simple xss like my exemple with the alert can by prevent if you pevent against html injection.
But more complex xss attack need more sofisticated prevention.
You will need a script like PHPIDS
or htmlpurifier.
But even with that, you still might be vulnerable, their no real way to be 100% protected, but find a good script (either phpids or htmlpurifier or a other) and update it as often as you can!

Uploading a file
You wish to give the user the possibility to attach a file on their post (yes still with the forum exemple... their's a lots of security risk in a forum)
First rule, is do not trust the user.
We all used this variable to validate the mime type of a file:
Code:
$_FILES["file"]["type"]
Well, sadly this variable can be modifier by the user, so you cannot trust it, insted use mime_content_type wich is deprecated and now you should use a this
Code:
//$file = a path to the file
$finfo = finfo_open(FILEINFO_MIME_TYPE);
return finfo_file($finfo, $file);
But since finfo_open isn't installed on all the server, this is a little snippets that could help you
Code:
        public static function getFileMime($file)
        {
               if (function_exists('finfo_file'))
               {
                       $finfo = finfo_open(FILEINFO_MIME_TYPE);
                       return finfo_file($finfo, $file);
               }
               if (function_exists('mime_content_type'))
               {
                       return mime_content_type($file);
               }
               return 'application/octet-stream';
        }
And you probably used this to limit the file size:
Code:
<input type="hidden" name="MAX_FILE_SIZE" value="500" />
One more time, this is really easy to modify (just take firebug and modify the html)
If you want to set a limite, you have to wait for the file to be completly uploaded than validate his size in php or you can set the limit in the php.ini file like this
Code:
    upload_max_filesize = 10M
    post_max_size = 10M

Now you know what is the real mime type of the file and if the file is small enough for you, you have to think where to save this file.
Most common place, is to save it outside the public_html directory, or if you cannot save it outside the public_html directory use a htaccess file to deny the file to everyone
Code:
deny from all
Like that, if the user uploaded a php script, or anything that is executable, he won't be able to do it directly.

In this directory, you should randomize the name, a good old
Code:
uniqid()
will do the tricks. Like that, if someone want to execute the file, and you forget to add the htaccess, he won't know where is the file.
If you wish to keep the original name (if the file can be downloaded for exemple) you should think of a sql table with the original name and your unique name.

If you are uploading a image, you should never keep the user original image. Insted you should make a copy of the image using the GD library and save the new image.
Image can contain more than just the image, it is possible to add php code inside a image, so when you display the image, it can inject some php code inside your code, and of course, this could be pretty dangerous.

Well, I think this is pretty complete, if you have any other security tricks, I would love to read them, remember, we are working to make a better internet for tomorow!
Hope this helped some of you.

Simple Register-Login-Logoff System


First make a database and upload those tables into your database:

users.sql
Code:
CREATE TABLE `users` (
`id` INT( 50 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 15 ) NOT NULL ,
`password` VARCHAR( 15 ) NOT NULL ,
`email` VARCHAR( 50 ) NOT NULL
)
index.php
Code:
<?php

//This will start a session
session_start();

$username $_SESSION['username'];
$password $_SESSION['password'];

//Check do we have username and password
if(!$username && !$password){
echo "Welcome Guest! <br> <a href=login.php>Login</a> | <a href=register.php>Register</a>";
}else{
echo "Welcome ".$username." (<a href=logout.php>Logout</a>)";
}
?>

Now let's make a register.php file
Code:
<?php

//This function will display the registration form
function register_form(){

$date date('D, M, Y');
echo "<form action='?act=register' method='post'>"
."Username: <input type='text' name='username' size='30'><br>"
."Password: <input type='password' name='password' size='30'><br>"
."Confirm your password: <input type='password' name='password_conf' size='30'><br>"
."Email: <input type='text' name='email' size='30'><br>"
."<input type='hidden' name='date' value='$date'>"
."<input type='submit' value='Register'>"
."</form>";

}

//This function will register users data
function register(){

//Connecting to database
$connect mysql_connect("host""username""password");
if(!$connect){
die(mysql_error());
}

//Selecting database
$select_db mysql_select_db("database"$connect);
if(!$select_db){
die(mysql_error());
}

//Collecting info
$username $_REQUEST['username'];
$password $_REQUEST['password'];
$pass_conf $_REQUEST['password_conf'];
$email $_REQUEST['email'];
$date $_REQUEST['date'];

//Here we will check do we have all inputs filled

if(empty($username)){
die("Please enter your username!<br>");
}

if(empty($password)){
die("Please enter your password!<br>");
}

if(empty($pass_conf)){
die("Please confirm your password!<br>");
}

if(empty($email)){
die("Please enter your email!");
}

//Let's check if this username is already in use

$user_check mysql_query("SELECT username FROM users WHERE username='$username'");
$do_user_check mysql_num_rows($user_check);

//Now if email is already in use

$email_check mysql_query("SELECT email FROM users WHERE email='$email'");
$do_email_check mysql_num_rows($email_check);

//Now display errors

if($do_user_check 0){
die("Username is already in use!<br>");
}

if($do_email_check 0){
die("Email is already in use!");
}

//Now let's check does passwords match

if($password != $pass_conf){
die("Passwords don't match!");
}


//If everything is okay let's register this user

$insert mysql_query("INSERT INTO users (username, password, email) VALUES ('$username', '$password', '$email')");
if(!$insert){
die("There's little problem: ".mysql_error());
}

echo $username.", you are now registered. Thank you!<br><a href=login.php>Login</a> | <a href=index.php>Index</a>";

}

switch($act){

default;
register_form();
break;

case "register";
register();
break;

}

?>
Now let's make a login page, login.php:
Code:
<?php
session_start();

//This displays your login form
function index(){

echo "<form action='?act=login' method='post'>" 
."Username: <input type='text' name='username' size='30'><br>"
."Password: <input type='password' name='password' size='30'><br>"
."<input type='submit' value='Login'>"
."</form>"

}

//This function will find and checks if your data is correct
function login(){

//Collect your info from login form
$username $_REQUEST['username'];
$password $_REQUEST['password'];


//Connecting to database
$connect mysql_connect("host""username""password");
if(!$connect){
die(mysql_error());
}

//Selecting database
$select_db mysql_select_db("database"$connect);
if(!$select_db){
die(mysql_error());
}

//Find if entered data is correct

$result mysql_query("SELECT * FROM users WHERE username='$username' AND password='$password'");
$row mysql_fetch_array($result);
$id $row['id'];

$select_user mysql_query("SELECT * FROM users WHERE id='$id'");
$row2 mysql_fetch_array($select_user);
$user $row2['username'];

if($username != $user){
die("Username is wrong!");
}


$pass_check mysql_query("SELECT * FROM users WHERE username='$username' AND id='$id'");
$row3 mysql_fetch_array($pass_check);
$email $row3['email'];
$select_pass mysql_query("SELECT * FROM users WHERE username='$username' AND id='$id' AND email='$email'");
$row4 mysql_fetch_array($select_pass);
$real_password $row4['password'];

if($password != $real_password){
die("Your password is wrong!");
}

//Now if everything is correct let's finish his/her/its login

session_register("username"$username);
session_register("password"$password);

echo "Welcome, ".$username." please continue on our <a href=index.php>Index</a>";

}

switch($act){

default;
index();
break;

case "login";
login();
break;

}
?>
And now.. logout.php
Code:
<?php
session_start();

//This function will destroy your session
session_destroy();
echo "You are now logged out! <a href=index.php>Index</a> or <a href=login.php>Login</a>";

?>
I hope it will helped you alot.