PHP Advent Calendar Day 4

04 Dec 2007

Today's entry is provided by James McGlinn.

James McGlinn

James McGlinn
James McGlinn is the CTO of Eventfinder (a major New Zealand entertainment site) and founder of the NZ PHP Users Group, now 500 strong. He is a member of the PHP Security Consortium and helps maintain user notes for the PHP documentation.
Auckland, New Zealand

In keeping with the Advent calendar theme, this entry touches on one aspect of the festive season that's difficult to miss. Shopping. Particularly online shopping and making sure the online payment experience is a safe and secure one for your visitors.

One aspect of online shopping that can be difficult to manage is making sure that your sensitive pages are correctly served by the secure server, without slowing your site (and your visitors' experience) down by serving pages over SSL that needn't be.

If you have only one or two pages that need to be secure (the checkout for example), it seems straightforward enough to simply specify absolute links to those pages on the SSL server. But how about when you add a few more secure pages (the login screen, admin system, or perhaps an alternate checkout for affiliate sales)? And, how do you redirect back to the non-secure server when your user navigates away from the checkout system, in order to increase responsiveness and reduce server load? Not to mention the headache that working with absolute (instead of relative) URLs brings to the development process.

The simplest answer to all of these issues is to create a single script, called within the header of your online store pages, to detect whether the page requested should be secure, and redirect to the appropriate server if necessary.

First, set up your configuration:

  1. <?php
  3. $hosts = array(
  4.     'secure' => '',
  5.     'nonsecure' => '',
  6. );
  8. $secure_pages = array(
  9.     '/checkout/',
  10.     '/login.php',
  11.     '/admin/',
  12. );
  14. ?>

The first array contains the hostnames for your secure and non-secure servers (assume both share the same document root). The second array contains a list of the directories or specific resources that must be served from the secure server. Non-secure requests that start with one of these strings will automatically be redirected to the secure server.

On to the code itself. First, we check the request against our list of secure pages and redirect to the secure server if necessary:

  1. <?php
  3. $clean = array();
  5. // Prevent whitespace characters in URL.
  6. if (ctype_print($_SERVER['REQUEST_URI'])) {
  7.     $clean['request_uri'] = $_SERVER['REQUEST_URI'];
  8. } else {
  9.     $clean['request_uri'] = '/';
  10. }
  12. if (empty($_SERVER['HTTPS']) && count($_POST) < 1) {
  13.     // Not using SSL and not posting data, so check if we should be using SSL.
  14.     foreach ($secure_pages as $secure_page) {
  15.         if (substr($clean['request_uri'], 0, strlen($secure_page)) == $secure_page) {
  16.             $new_url = sprintf('https://%s%s', $hosts['secure'], $clean['request_uri']);
  17.             header('Location: ' . $new_url);
  18.             exit;
  19.         }
  20.     }
  21. }
  23. ?>

This will redirect visitors transparently to the secure server, safe in the knowledge that their credit card details can't be intercepted en route to your store. But, what about when the checkout process is complete, or they abandon the procedure part way through to add last minute items to their Christmas basket of goodies? By adding to the code above, we can redirect users still on the secure server back to the non-secure server as required:

  1. <?php
  3. elseif (!empty($_SERVER['HTTPS']) && count($_POST) < 1) {
  4.     // Using SSL and not posting data.
  5.     $dont_redirect = FALSE;
  6.     foreach ($secure_pages as $secure_page) {
  7.         if (substr($clean['request_uri'], 0, strlen($secure_page)) == $secure_page) {
  8.             $dont_redirect = TRUE;
  9.         }
  10.     }
  12.     if ($dont_redirect === FALSE) {
  13.         // Redirect.
  14.         $new_url = sprintf('http://%s%s', $hosts['nonsecure'], $clean['request_uri']);
  15.         header('Location: ' . $new_url);
  16.         exit;
  17.     }
  18. }
  20. ?>

This will redirect the user back to the faster non-secure server for those requests where security isn't an issue, without any more intervention on your part.

You'll notice in each of these code samples that we don't automatically redirect POST requests. This is because any data accompanying the POST request would be lost. I leave it as an exercise for you to determine what should be done in that instance and handle the condition accordingly.