Published using Google Docs
CREATING BASIC SECURE WEBSITE
Updated automatically every 5 minutes

CREATING BASIC SECURE WEBSITE

INTRODUCTION

This tutorial is based on http://forums.devshed.com/php-faqs-stickies-167/program-basic-secure-login-system-using-php-mysql-891201.html

STEPS

1) Site Structure.

2) Codes

2.1) common.php

<?php

    // These variables define the connection information for your MySQL database

    $username = "root";

    $password = "root";

    $host = "localhost";

    $dbname = "secuba";

    // UTF-8 is a character encoding scheme that allows you to conveniently store

    // a wide varienty of special characters, like ¢ or €, in your database.

    // By passing the following $options array to the database connection code we

    // are telling the MySQL server that we want to communicate with it using UTF-8

    // See Wikipedia for more information on UTF-8:

    // http://en.wikipedia.org/wiki/UTF-8

    $options = array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8');

     

    // A try/catch statement is a common method of error handling in object oriented code.

    // First, PHP executes the code within the try block.  If at any time it encounters an

    // error while executing that code, it stops immediately and jumps down to the

    // catch block.  For more detailed information on exceptions and try/catch blocks:

    // http://us2.php.net/manual/en/language.exceptions.php

    try

    {

        // This statement opens a connection to your database using the PDO library

        // PDO is designed to provide a flexible interface between PHP and many

        // different types of database servers.  For more information on PDO:

        // http://us2.php.net/manual/en/class.pdo.php

        $db = new PDO("mysql:host={$host};dbname={$dbname};charset=utf8", $username, $password, $options);

    }

    catch(PDOException $ex)

    {

        // If an error occurs while opening a connection to your database, it will

        // be trapped here.  The script will output an error and stop executing.

        // Note: On a production website, you should not output $ex->getMessage().

        // It may provide an attacker with helpful information about your code

        // (like your database username and password).

        die("Failed to connect to the database: " . $ex->getMessage());

    }

     

    // This statement configures PDO to throw an exception when it encounters

    // an error.  This allows us to use try/catch blocks to trap database errors.

    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

     

    // This statement configures PDO to return database rows from your database using an associative

    // array.  This means the array will have string indexes, where the string value

    // represents the name of the column in your database.

    $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

     

    // This block of code is used to undo magic quotes.  Magic quotes are a terrible

    // feature that was removed from PHP as of PHP 5.4.  However, older installations

    // of PHP may still have magic quotes enabled and this code is necessary to

    // prevent them from causing problems.  For more information on magic quotes:

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

    if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())

    {

        function undo_magic_quotes_gpc(&$array)

        {

            foreach($array as &$value)

            {

                if(is_array($value))

                {

                    undo_magic_quotes_gpc($value);

                }

                else

                {

                    $value = stripslashes($value);

                }

            }

        }

     

        undo_magic_quotes_gpc($_POST);

        undo_magic_quotes_gpc($_GET);

        undo_magic_quotes_gpc($_COOKIE);

    }

     

    // This tells the web browser that your content is encoded using UTF-8

    // and that it should submit content back to you using UTF-8

    header('Content-Type: text/html; charset=utf-8');

     

    // This initializes a session.  Sessions are used to store information about

    // a visitor from one web page visit to the next.  Unlike a cookie, the information is

    // stored on the server-side and cannot be modified by the visitor.  However,

    // note that in most cases sessions do still use cookies and require the visitor

    // to have cookies enabled.  For more information about sessions:

    // http://us.php.net/manual/en/book.session.php

    session_start();

    // Note that it is a good practice to NOT end your PHP files with a closing PHP tag.

    // This prevents trailing newlines on the file from being included in your output,

    // which can cause problems with redirecting users.

2.2) edit_account.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // At the top of the page we check to see whether the user is logged in or not

    if(empty($_SESSION['user']))

    {

        // If they are not, we redirect them to the login page.

        header("Location: login.php");

         

        // Remember that this die statement is absolutely critical.  Without it,

        // people can view your members-only content without logging in.

        die("Redirecting to login.php");

    }

     

    // This if statement checks to determine whether the edit form has been submitted

    // If it has, then the account updating code is run, otherwise the form is displayed

    if(!empty($_POST))

    {

        // Make sure the user entered a valid E-Mail address

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

        {

            die("Invalid E-Mail Address");

        }

         

        // If the user is changing their E-Mail address, we need to make sure that

        // the new value does not conflict with a value that is already in the system.

        // If the user is not changing their E-Mail address this check is not needed.

        if($_POST['email'] != $_SESSION['user']['email'])

        {

            // Define our SQL query

            $query = "

                SELECT

                    1

                FROM users

                WHERE

                    email = :email

            ";

             

            // Define our query parameter values

            $query_params = array(

                ':email' => $_POST['email']

            );

             

            try

            {

                // Execute the query

                $stmt = $db->prepare($query);

                $result = $stmt->execute($query_params);

            }

            catch(PDOException $ex)

            {

                // Note: On a production website, you should not output $ex->getMessage().

                // It may provide an attacker with helpful information about your code.  

                die("Failed to run query: " . $ex->getMessage());

            }

             

            // Retrieve results (if any)

            $row = $stmt->fetch();

            if($row)

            {

                die("This E-Mail address is already in use");

            }

        }

         

        // If the user entered a new password, we need to hash it and generate a fresh salt

        // for good measure.

        if(!empty($_POST['password']))

        {

            $salt = dechex(mt_rand(0, 2147483647)) . dechex(mt_rand(0, 2147483647));

            $password = hash('sha256', $_POST['password'] . $salt);

            for($round = 0; $round < 65536; $round++)

            {

                $password = hash('sha256', $password . $salt);

            }

        }

        else

        {

            // If the user did not enter a new password we will not update their old one.

            $password = null;

            $salt = null;

        }

         

        // Initial query parameter values

        $query_params = array(

            ':email' => $_POST['email'],

            ':user_id' => $_SESSION['user']['id'],

        );

         

        // If the user is changing their password, then we need parameter values

        // for the new password hash and salt too.

        if($password !== null)

        {

            $query_params[':password'] = $password;

            $query_params[':salt'] = $salt;

        }

         

        // Note how this is only first half of the necessary update query.  We will dynamically

        // construct the rest of it depending on whether or not the user is changing

        // their password.

        $query = "

            UPDATE users

            SET

                email = :email

        ";

         

        // If the user is changing their password, then we extend the SQL query

        // to include the password and salt columns and parameter tokens too.

        if($password !== null)

        {

            $query .= "

                , password = :password

                , salt = :salt

            ";

        }

         

        // Finally we finish the update query by specifying that we only wish

        // to update the one record with for the current user.

        $query .= "

            WHERE

                id = :user_id

        ";

         

        try

        {

            // Execute the query

            $stmt = $db->prepare($query);

            $result = $stmt->execute($query_params);

        }

        catch(PDOException $ex)

        {

            // Note: On a production website, you should not output $ex->getMessage().

            // It may provide an attacker with helpful information about your code.  

            die("Failed to run query: " . $ex->getMessage());

        }

         

        // Now that the user's E-Mail address has changed, the data stored in the $_SESSION

        // array is stale; we need to update it so that it is accurate.

        $_SESSION['user']['email'] = $_POST['email'];

         

        // This redirects the user back to the members-only page after they register

        header("Location: private.php");

         

        // Calling die or exit after performing a redirect using the header function

        // is critical.  The rest of your PHP script will continue to execute and

        // will be sent to the user if you do not die or exit.

        die("Redirecting to private.php");

    }

     

?>

<h1>Edit Account</h1>

<form action="edit_account.php" method="post">

    Username:<br />

    <b><?php echo htmlentities($_SESSION['user']['username'], ENT_QUOTES, 'UTF-8'); ?></b>

    <br /><br />

    E-Mail Address:<br />

    <input type="text" name="email" value="<?php echo htmlentities($_SESSION['user']['email'], ENT_QUOTES, 'UTF-8'); ?>" />

    <br /><br />

    Password:<br />

    <input type="password" name="password" value="" /><br />

    <i>(leave blank if you do not want to change your password)</i>

    <br /><br />

    <input type="submit" value="Update Account" />

</form>

2.3) login.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // This variable will be used to re-display the user's username to them in the

    // login form if they fail to enter the correct password.  It is initialized here

    // to an empty value, which will be shown if the user has not submitted the form.

    $submitted_username = '';

     

    // This if statement checks to determine whether the login form has been submitted

    // If it has, then the login code is run, otherwise the form is displayed

    if(!empty($_POST))

    {

        // This query retreives the user's information from the database using

        // their username.

        $query = "

            SELECT

                id,

                username,

                password,

                salt,

                email

            FROM users

            WHERE

                username = :username

        ";

         

        // The parameter values

        $query_params = array(

            ':username' => $_POST['username']

        );

         

        try

        {

            // Execute the query against the database

            $stmt = $db->prepare($query);

            $result = $stmt->execute($query_params);

        }

        catch(PDOException $ex)

        {

            // Note: On a production website, you should not output $ex->getMessage().

            // It may provide an attacker with helpful information about your code.  

            die("Failed to run query: " . $ex->getMessage());

        }

         

        // This variable tells us whether the user has successfully logged in or not.

        // We initialize it to false, assuming they have not.

        // If we determine that they have entered the right details, then we switch it to true.

        $login_ok = false;

         

        // Retrieve the user data from the database.  If $row is false, then the username

        // they entered is not registered.

        $row = $stmt->fetch();

        if($row)

        {

            // Using the password submitted by the user and the salt stored in the database,

            // we now check to see whether the passwords match by hashing the submitted password

            // and comparing it to the hashed version already stored in the database.

            $check_password = hash('sha256', $_POST['password'] . $row['salt']);

            for($round = 0; $round < 65536; $round++)

            {

                $check_password = hash('sha256', $check_password . $row['salt']);

            }

             

            if($check_password === $row['password'])

            {

                // If they do, then we flip this to true

                $login_ok = true;

            }

        }

         

        // If the user logged in successfully, then we send them to the private members-only page

        // Otherwise, we display a login failed message and show the login form again

        if($login_ok)

        {

            // Here I am preparing to store the $row array into the $_SESSION by

            // removing the salt and password values from it.  Although $_SESSION is

            // stored on the server-side, there is no reason to store sensitive values

            // in it unless you have to.  Thus, it is best practice to remove these

            // sensitive values first.

            unset($row['salt']);

            unset($row['password']);

             

            // This stores the user's data into the session at the index 'user'.

            // We will check this index on the private members-only page to determine whether

            // or not the user is logged in.  We can also use it to retrieve

            // the user's details.

            $_SESSION['user'] = $row;

             

            // Redirect the user to the private members-only page.

            header("Location: private.php");

            die("Redirecting to: private.php");

        }

        else

        {

            // Tell the user they failed

            print("Login Failed.");

             

            // Show them their username again so all they have to do is enter a new

            // password.  The use of htmlentities prevents XSS attacks.  You should

            // always use htmlentities on user submitted values before displaying them

            // to any users (including the user that submitted them).  For more information:

            // http://en.wikipedia.org/wiki/XSS_attack

            $submitted_username = htmlentities($_POST['username'], ENT_QUOTES, 'UTF-8');

        }

    }

     

?>

<h1>Login</h1>

<form action="login.php" method="post">

    Username:<br />

    <input type="text" name="username" value="<?php echo $submitted_username; ?>" />

    <br /><br />

    Password:<br />

    <input type="password" name="password" value="" />

    <br /><br />

    <input type="submit" value="Login" />

</form>

<a href="register.php">Register</a>

2.4) logout.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // We remove the user's data from the session

    unset($_SESSION['user']);

     

    // We redirect them to the login page

    header("Location: login.php");

    die("Redirecting to: login.php");

2.5) memberlist.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // At the top of the page we check to see whether the user is logged in or not

    if(empty($_SESSION['user']))

    {

        // If they are not, we redirect them to the login page.

        header("Location: login.php");

         

        // Remember that this die statement is absolutely critical.  Without it,

        // people can view your members-only content without logging in.

        die("Redirecting to login.php");

    }

     

    // Everything below this point in the file is secured by the login system

     

    // We can retrieve a list of members from the database using a SELECT query.

    // In this case we do not have a WHERE clause because we want to select all

    // of the rows from the database table.

    $query = "

        SELECT

            id,

            username,

            email

        FROM users

    ";

     

    try

    {

        // These two statements run the query against your database table.

        $stmt = $db->prepare($query);

        $stmt->execute();

    }

    catch(PDOException $ex)

    {

        // Note: On a production website, you should not output $ex->getMessage().

        // It may provide an attacker with helpful information about your code.  

        die("Failed to run query: " . $ex->getMessage());

    }

         

    // Finally, we can retrieve all of the found rows into an array using fetchAll

    $rows = $stmt->fetchAll();

?>

<h1>Memberlist</h1>

<table>

    <tr>

        <th>ID</th>

        <th>Username</th>

        <th>E-Mail Address</th>

    </tr>

    <?php foreach($rows as $row): ?>

        <tr>

            <td><?php echo $row['id']; ?></td> <!-- htmlentities is not needed here because $row['id'] is always an integer -->

            <td><?php echo htmlentities($row['username'], ENT_QUOTES, 'UTF-8'); ?></td>

            <td><?php echo htmlentities($row['email'], ENT_QUOTES, 'UTF-8'); ?></td>

        </tr>

    <?php endforeach; ?>

</table>

<a href="private.php">Go Back</a><br />

2.6) private.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // At the top of the page we check to see whether the user is logged in or not

    if(empty($_SESSION['user']))

    {

        // If they are not, we redirect them to the login page.

        header("Location: login.php");

         

        // Remember that this die statement is absolutely critical.  Without it,

        // people can view your members-only content without logging in.

        die("Redirecting to login.php");

    }

     

    // Everything below this point in the file is secured by the login system

     

    // We can display the user's username to them by reading it from the session array.  Remember that because

    // a username is user submitted content we must use htmlentities on it before displaying it to the user.

?>

Hello <?php echo htmlentities($_SESSION['user']['username'], ENT_QUOTES, 'UTF-8'); ?>, secret content!<br />

<a href="memberlist.php">Memberlist</a><br />

<a href="edit_account.php">Edit Account</a><br />

<a href="logout.php">Logout</a>

2.7) register.php

<?php

    // First we execute our common code to connection to the database and start the session

    require("common.php");

     

    // This if statement checks to determine whether the registration form has been submitted

    // If it has, then the registration code is run, otherwise the form is displayed

    if(!empty($_POST))

    {

        // Ensure that the user has entered a non-empty username

        if(empty($_POST['username']))

        {

            // Note that die() is generally a terrible way of handling user errors

            // like this.  It is much better to display the error with the form

            // and allow the user to correct their mistake.  However, that is an

            // exercise for you to implement yourself.

            die("Please enter a username.");

        }

         

        // Ensure that the user has entered a non-empty password

        if(empty($_POST['password']))

        {

            die("Please enter a password.");

        }

         

        // Make sure the user entered a valid E-Mail address

        // filter_var is a useful PHP function for validating form input, see:

        // http://us.php.net/manual/en/function.filter-var.php

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

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

        {

            die("Invalid E-Mail Address");

        }

         

        // We will use this SQL query to see whether the username entered by the

        // user is already in use.  A SELECT query is used to retrieve data from the database.

        // :username is a special token, we will substitute a real value in its place when

        // we execute the query.

        $query = "

            SELECT

                1

            FROM users

            WHERE

                username = :username

        ";

         

        // This contains the definitions for any special tokens that we place in

        // our SQL query.  In this case, we are defining a value for the token

        // :username.  It is possible to insert $_POST['username'] directly into

        // your $query string; however doing so is very insecure and opens your

        // code up to SQL injection exploits.  Using tokens prevents this.

        // For more information on SQL injections, see Wikipedia:

        // http://en.wikipedia.org/wiki/SQL_Injection

        $query_params = array(

            ':username' => $_POST['username']

        );

         

        try

        {

            // These two statements run the query against your database table.

            $stmt = $db->prepare($query);

            $result = $stmt->execute($query_params);

        }

        catch(PDOException $ex)

        {

            // Note: On a production website, you should not output $ex->getMessage().

            // It may provide an attacker with helpful information about your code.  

            die("Failed to run query: " . $ex->getMessage());

        }

         

        // The fetch() method returns an array representing the "next" row from

        // the selected results, or false if there are no more rows to fetch.

        $row = $stmt->fetch();

         

        // If a row was returned, then we know a matching username was found in

        // the database already and we should not allow the user to continue.

        if($row)

        {

            die("This username is already in use");

        }

         

        // Now we perform the same type of check for the email address, in order

        // to ensure that it is unique.

        $query = "

            SELECT

                1

            FROM users

            WHERE

                email = :email

        ";

         

        $query_params = array(

            ':email' => $_POST['email']

        );

         

        try

        {

            $stmt = $db->prepare($query);

            $result = $stmt->execute($query_params);

        }

        catch(PDOException $ex)

        {

            die("Failed to run query: " . $ex->getMessage());

        }

         

        $row = $stmt->fetch();

         

        if($row)

        {

            die("This email address is already registered");

        }

         

        // An INSERT query is used to add new rows to a database table.

        // Again, we are using special tokens (technically called parameters) to

        // protect against SQL injection attacks.

        $query = "

            INSERT INTO users (

                username,

                password,

                salt,

                email

            ) VALUES (

                :username,

                :password,

                :salt,

                :email

            )

        ";

         

        // A salt is randomly generated here to protect again brute force attacks

        // and rainbow table attacks.  The following statement generates a hex

        // representation of an 8 byte salt.  Representing this in hex provides

        // no additional security, but makes it easier for humans to read.

        // For more information:

        // http://en.wikipedia.org/wiki/Salt_%28cryptography%29

        // http://en.wikipedia.org/wiki/Brute-force_attack

        // http://en.wikipedia.org/wiki/Rainbow_table

        $salt = dechex(mt_rand(0, 2147483647)) . dechex(mt_rand(0, 2147483647));

         

        // This hashes the password with the salt so that it can be stored securely

        // in your database.  The output of this next statement is a 64 byte hex

        // string representing the 32 byte sha256 hash of the password.  The original

        // password cannot be recovered from the hash.  For more information:

        // http://en.wikipedia.org/wiki/Cryptographic_hash_function

        $password = hash('sha256', $_POST['password'] . $salt);

         

        // Next we hash the hash value 65536 more times.  The purpose of this is to

        // protect against brute force attacks.  Now an attacker must compute the hash 65537

        // times for each guess they make against a password, whereas if the password

        // were hashed only once the attacker would have been able to make 65537 different  

        // guesses in the same amount of time instead of only one.

        for($round = 0; $round < 65536; $round++)

        {

            $password = hash('sha256', $password . $salt);

        }

         

        // Here we prepare our tokens for insertion into the SQL query.  We do not

        // store the original password; only the hashed version of it.  We do store

        // the salt (in its plaintext form; this is not a security risk).

        $query_params = array(

            ':username' => $_POST['username'],

            ':password' => $password,

            ':salt' => $salt,

            ':email' => $_POST['email']

        );

         

        try

        {

            // Execute the query to create the user

            $stmt = $db->prepare($query);

            $result = $stmt->execute($query_params);

        }

        catch(PDOException $ex)

        {

            // Note: On a production website, you should not output $ex->getMessage().

            // It may provide an attacker with helpful information about your code.  

            die("Failed to run query: " . $ex->getMessage());

        }

         

        // This redirects the user back to the login page after they register

        header("Location: login.php");

         

        // Calling die or exit after performing a redirect using the header function

        // is critical.  The rest of your PHP script will continue to execute and

        // will be sent to the user if you do not die or exit.

        die("Redirecting to login.php");

    }

     

?>

<h1>Register</h1>

<form action="register.php" method="post">

    Username:<br />

    <input type="text" name="username" value="" />

    <br /><br />

    E-Mail:<br />

    <input type="text" name="email" value="" />

    <br /><br />

    Password:<br />

    <input type="password" name="password" value="" />

    <br /><br />

    <input type="submit" value="Register" />

</form>

3) Database Preparation.

3.1) Run UwAmp

3.2) Click the PHPMyAdmin button to browse the PHPMyAdmin page.

3.3) Your default Web Browser will automatically open the http://localhost/mysql/index.php

(If it is not in English) Select Language as English.

3.4) Click on SQL button  to enter SQL Commands.

3.5) Create a new database “secuba”.

CREATE DATABASE `secuba`;

3.6) Create table “users”.

CREATE TABLE `users` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,

  `password` char(64) COLLATE utf8_unicode_ci NOT NULL,

  `salt` char(16) COLLATE utf8_unicode_ci NOT NULL,

  `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,

  PRIMARY KEY (`id`),

  UNIQUE KEY `username` (`username`),

  UNIQUE KEY `email` (`email`)

) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;

4) Test.

4.1) Register an account.

4.2) Login to the system.

4.3) Navigate the pages.