While writing code for desktop applications is my daily job, i'm discovering step by step the goodies and the hassles of web development with PHP. On this site i would like to share the encountered problems and the solutions i found, in hope that it will help someone building his own website.
If you should have problems, questions or suggestions about the functions below, or if you simply find them useful, don't hesitate to send me an email to .
What i'm missing most in PHP, is the benefit of a strong typed language. Dynamic typing may have it's advantages, but would you have thought following comparisons will give back true? PHP makes it possible...
Of course you can use the === operator, to check values and their types. Since PHP doesn't support you well with controlling types explicitly, i found it to be of no much use. That was the point when i started writing a class covering all the things i wished to be built-in in the PHP language.
/** * Checks if two values are equal. In contrast to the == operator, * the values are considered different, if: * - one value is null and the other not, or * - one value is an empty string and the other not * This helps avoid strange behavier with PHP's type juggling, * all these expressions would return true: * 'abc' == 0; 0 == null; '' == null; 1 == '1y?z'; * @param mixed $value1 * @param mixed $value2 * @return boolean True if values are equal, otherwise false. */ function sto_equals($value1, $value2) { // identical in value and type if ($value1 === $value2) $result = true; // one is null, the other not else if (is_null($value1) || is_null($value2)) $result = false; // one is an empty string, the other not else if (($value1 === '') || ($value2 === '')) $result = false; // identical in value and different in type else { $result = ($value1 == $value2); // test for wrong implicit string conversion, when comparing a // string with a numeric type. only accept valid numeric strings. if ($result) { $isNumericType1 = is_int($value1) || is_float($value1); $isNumericType2 = is_int($value2) || is_float($value2); $isStringType1 = is_string($value1); $isStringType2 = is_string($value2); if ($isNumericType1 && $isStringType2) $result = is_numeric($value2); else if ($isNumericType2 && $isStringType1) $result = is_numeric($value1); } } return $result; }
Most browsers today will help protecting your site from malicious attacks, but you have to tell them they should. A widely supported method is setting the X-Frame-Options. Setting this option, the browser will not allow other sites to display your page inside an iframe. This protects against Clickjacking attacks and should be used on all sensitive pages like the login page.
/** * Adds the X-Frame-Options to the HTTP header. */ function sto_setXFrameOptions() { header('X-Frame-Options: DENY'); // or SAMEORIGIN }
People using Firefox 4 and later, will benefit automatically, when a website sends a Content-Security-Policy (CSP) within the HTTP header. With a CSP you can specify from which locations you accept javascript, which sites are allowed to show your page inside an iframe and many other things. If a browser supports CSP, this can be an effective protection against Cross-Site-Scripting.
The implementation in PHP is incredible easy, though some problems may arise from inline JavaScript. But let's see an example:
/**
* Adds the Content-Security-Policy to the HTTP header. JavaScript will
* be restricted to the same domain as the page itself.
* @param bool $allowInlineScript Set parameter to true, if you want to
* allow javascript contained in the page itself (no separate *.js file).
*/
function sto_setContentSecurityPolicy($allowInlineScript)
{
if ($allowInlineScript)
header("X-Content-Security-Policy: allow 'self'; options inline-script");
else
header("X-Content-Security-Policy: allow 'self'");
}
After reading a lot of articles about secure session handling, describing mostly what you shouldn't do, i was a bit confused, because it seemed not possible to do it right. Finally i found a way, i called the noMim cookie, it's a way to separate the two concerns "maintaining the PHP session" and "authentication". This solves a lot of problems, but let's have look at it step by step.
First we absolutely need a HTTPS connection with SSL encryption to deliver sensitive data, otherwise all data is sent clear text over the internet. The session-id should be stored in a cookie, because passing it along the URL makes session-fixation much to easy. Then you should always send the cookie encrypted. Well, sounds easy enought doesn't it?
A very common scenario is a web shop, where you collect items into a shopping cart, then you log in, and before you send the order, maybe you want to collect further items. It makes sense, that showing things like the AGB is done with the unsecure HTTP protocoll. Sending sensitive data like login and credit card information require a secure HTTPS connection.
HTTPS makes sure, that nobody between client and server can eavesdrop our communication and prevents a man-in-the-middle attack. Unfortunately this doesn't apply to the session-cookie, the same cookie is sent encrypted for HTTPS pages and unencrypted for HTTP pages. Even when getting an image with HTTP, the cookie is sent along plaintext.
PHP offers the function session_set_cookie_params(...) with the parameter $secure. Setting this parameter to true, the session-cookie will only be sent back to secure HTTPS pages. This is what we need, but it leaves us to the problem that we loose our session, when we switch to an unsecure HTTP page. There seems to be no solution, where the session-cookie is secure and switching between secure and unsecure pages is possible.
The idea of the noMim (no-man-in-the-middle) cookie is, that when the user enters his password (increases his access privileges), we create a second cookie additionally to the unsecure session-cookie, and make sure that only encrypted HTTPS pages have access to it. Of course the login page itself has to use HTTPS.
https://www.example.com/login.php <?php session_start(); // regenerate session id to make session fixation more difficult session_regenerate_id(true); // generate random code for the noMim cookie (no man in the middle) $noMimCode = md5(uniqid(mt_rand(), true)); // create cookie, which will be sent back only to HTTPS pages setcookie('noMim', $noMimCode, 0, '/', '', true, true); $_SESSION['noMim'] = $noMimCode; print('<h1>login</h1>'); ... ?>
Now every page can use the unsecure session-cookie to maintain the session, but pages with sensitive information can check for the secure noMim cookie.
https://www.example.com/secret.php <?php session_start(); // check that the noMim cookie exists // and is equal to the code stored in the session. $pageIsSecure = (!empty($_COOKIE['noMim'])) && ($_COOKIE['noMim'] === $_SESSION['noMim']); if (!$pageIsSecure) { // go back to the login page } print('<h1>private</h1>'); ... ?>
An attacker could manipulate the session cookie, but he never has access to the noMim cookie. Only the person who entered the password, can own the noMim cookie, it's always sent over encrypted HTTPS connections.
Using the session-cookie exclusively for maintaining the session and separating this concern from authentication, is a good thing to do, even when we don't switch between HTTPS and HTTP pages.
To calculate the spheric distance between two points on the earth (great-circle distance), one can use the Haversine formula. This formula is more stable for calculating small distances, regarding rounding errors.
/** * Calculates the great-circle distance between two points, with * the Haversine formula. * @param float $latitudeFrom Latitude of start point in [deg decimal] * @param float $longitudeFrom Longitude of start point in [deg decimal] * @param float $latitudeTo Latitude of target point in [deg decimal] * @param float $longitudeTo Longitude of target point in [deg decimal] * @param float $earthRadius Mean earth radius in [m] * @return float Distance between points in [m] (same as earthRadius) */ function haversineGreatCircleDistance( $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000) { // convert from degrees to radians $latFrom = deg2rad($latitudeFrom); $lonFrom = deg2rad($longitudeFrom); $latTo = deg2rad($latitudeTo); $lonTo = deg2rad($longitudeTo); $latDelta = $latTo - $latFrom; $lonDelta = $lonTo - $lonFrom; $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) + cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2))); return $angle * $earthRadius; }