Explicit Code & QA
So, who are you, anyway? Bryan C. Geraghty Security Consultant at Security PS @archwisp I’m a Sr. PHP developer with a systems and security engineering background - turned application security consultant
Remember, layers Simpler is easier to test Don’t make assumptions Compromised browser = game over
These are not specifically security activities but they produce more stable code
Don’t rely on the server’s configuration Your bootstrap should Absolute Minimum set the defaults for any  Include paths PHP core functionality  Error reporting that your application is  Time zones using. These must be customizable for every environment e.g. (dev, staging, live)
Example Bootstrap <?php // All environment configuration for this application is done from this file ini_set('display_errors', 0); ini_set('error_reporting', -1); date_default_timezone_set('UTC'); $rootPath = dirname(__FILE__); set_include_path(sprintf( '%s%s%s%s', $rootPath, PATH_SEPARATOR, $rootPath, '/third-party' )); require_once('MindFrame2/AutoLoad.php'); MindFrame2_AutoLoad::install();
Don’t use global constants, variables, or registries  You can’t be certain that nobody else will ever use the same global constant name  Tracking down where a value is assigned to a global variable is usually very difficult  Global variables remove control of that variable from the current scope  Global registries are really just fancy global variables and the same problems apply
Value Reference Rules  Values should only be referenced in one of the following ways:  A direct reference in the current scope: $limit = $this->_objectPool->getConfig()- >getDefaultLimit();  A parameter: public function getUsers($limit) { return $this->_objectPool- >getMapper(‘User’)->fetchAll($limit); }
Don’t use function parameter defaults "There should be one-- and preferably only one -- obvious way to do it." - Tim Peters  Reduces understanding from the caller perspective  Often add unnecessary complexity  Represents the most common use-case  Other use-cases are often ignored entirely  Changing the default breaks all implementations  Adding parameters breaks default implementations
Default parameter example class User { public function __construct($name, $email, $status = self::STATUS_ACTIVE) { $this->_name = $name; $this->_email = $email; $this->_status = $status; } } function createUser($name, $email, $status) { $user = new User($name, email); return $this->_objectPoool->getMapper(‘User’)->create($user); }
Use explicit operators whenever possible This was actual production code. if ($sincedate && (int)$sincedate) { $param["UpdatedSinceDate"] = date("Y-m-d H:i:s", strtotime(time() - ((int)$sincedate)*60*60); } elseif ($sincedate && strtotime($sincedate)) { $param["UpdatedSinceDate"] = date("Y-m-d H:i:s", strtotime($sincedate)); } elseif(!isset($param["UpdatedSinceDate"])) { exit ("Invalid param UpdatedSinceDate"); } Do you think this code worked as the author intended when $sincedate was set to '2011-05-31'?
Use constants for possible values Constants cannot be re-defined, so using a constant for configuration limits your code to only ever be able to run in one configuration in a given stack. // Bad: This code cannot execute differently in the same stack define(‘PDO_FETCH_MODE’, 2); $dbi->fetchAll(PDO_FETCH_MODE); // Better: The value can change but this literal value means nothing without // referencing the documentation $dbi->fetchAll(2); // Best: Implemented using a meaningful constant which represents a *possible* value $dbi->fetchAll(PDO::FETCH_ASSOC);
Use wrappers for super-globals  $_REQUEST, $_GET, $_SESSION, and $_FILES are the most commonly used PHP super-globals but there are others  It’s easy to create some serious security problems  It’s best to use a wrapper for interacting with these variables
Simulate strong/static types PHP is dynamically typed, so we cannot do true strong or static typing but we can put controls in place which provide the same protection. private function _buildSelectDatabaseTableSql($database_name, $table_name, array $select_data, array $order_by_columns, $offset, $limit) { MindFrame2_Core::assertArgumentIsNotBlank($database_name, 1, 'database_name'); MindFrame2_Core::assertArgumentIsNotBlank($table_name, 2, 'table_name'); MindFrame2_Core::assertArgumentIsInt($offset, 5, 'offset'); MindFrame2_Core::assertArgumentIsInt($limit, 6, 'limit'); $skel = "SELECTn %snFROMn %s.%sn%s%s%s%s;"; $sql = sprintf($skel, $this->_buildSelectTableSqlFieldClause($table_name), $this->getSharedModule()->escapeDbElementName($database_name), $this->getSharedModule()->escapeDbElementName($table_name), $this->_buildSelectTableSqlJoinClause( $this->getSharedModule()->getDatabase()->getName(), $table_name), $this->_buildSelectTableSqlWhereClause($table_name, $select_data), $this->_buildSelectTableSqlOrderByClause( $table_name, $order_by_columns), $this->_buildSelectTableSqlLimitClause($offset, $limit)); return $sql; }
Example Typing Assertions public static function assertArgumentIsInt($value, $position, $name) { if (!is_int($value)) { $skel = 'Expected integer value for argument #%d (%s), %s given'; $message = sprintf($skel, $position, $name, gettype($value)); throw new InvalidArgumentException($message); } } public static function assertArgumentIsNotBlank($value, $position, $name) { if (trim($value) === '') { $skel = 'Argument #%d (%s) cannot be blank'; $message = sprintf($skel, $position, $name); throw new InvalidArgumentException($message); } }
Use PHPMD & CodeSniffer This is the ./bin/codecheck script in MindFrame2 #!/bin/bash WD="`/usr/bin/dirname $0`/../"; echo Changing to working directory: $WD; cd $WD; if [ -z $1 ]; then DIR='.'; else DIR=$1; fi phpmd --exclude coverage $DIR text codesize,unusedcode,naming,design phpcs --ignore=coverage --standard=`pwd`/Standards/MindFrame2 $DIR phpcs --ignore=coverage --report=source --standard=`pwd`/Standards/MindFrame2 $DIR
Example PHPMD Output bryan@ubuntu-virtual ~/Code/MindFrame2 [130] $ ./bin/codecheck Dbms/Dbi Changing to working directory: ./bin/../ /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:128 Avoid unused local variables such as '$partner'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:185 Avoid unused local variables such as '$data'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:199 Avoid unused local variables such as '$index'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:240 Avoid unused private methods such as '_buildReplicaDatabaseSuffix'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php:25 This class has too many methods, consider refactoring it. /home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php:150 Avoid unused parameters such as '$options'.
Example PHPCS Output FILE: /home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php -------------------------------------------------------------------------------- FOUND 6 ERROR(S) AFFECTING 6 LINE(S) -------------------------------------------------------------------------------- 39 | ERROR | Public member variables ("_connection") are forbidden 62 | ERROR | Whitespace found at end of line 144 | ERROR | Whitespace found at end of line 160 | ERROR | Line exceeds maximum limit of 80 characters; contains 97 | | characters 195 | ERROR | Whitespace found at end of line 298 | ERROR | Line exceeds maximum limit of 80 characters; contains 81 | | characters --------------------------------------------------------------------------------
Unit testing / TDD  Test critical functionality at a minimum  Add edge cases to regression tests as they are discovered  Test-driven development only works if the entire team is dedicated to it  I personally don’t trust code that isn’t regression-tested; even if it was written by me
Continuous Integration  Automated system for code analysis and regression testing  There are a lot of continuous integration systems for PHP  If your tests aren’t automated, very few people will know or care when they break
Deployment  Manually copying files is riddled with problems  Tagging releases and re-deploying is a complicated and time-consuming process  Deployment systems automate the build and deployment process
ACLs MAC Thread limits Unwanted services
If you’re interested in an application security career, come talk with me.

Creating "Secure" PHP Applications, Part 1, Explicit Code & QA

  • 1.
  • 2.
    So, who areyou, anyway? Bryan C. Geraghty Security Consultant at Security PS @archwisp I’m a Sr. PHP developer with a systems and security engineering background - turned application security consultant
  • 3.
    Remember, layers Simpler iseasier to test Don’t make assumptions Compromised browser = game over
  • 4.
    These are notspecifically security activities but they produce more stable code
  • 5.
    Don’t rely onthe server’s configuration Your bootstrap should Absolute Minimum set the defaults for any  Include paths PHP core functionality  Error reporting that your application is  Time zones using. These must be customizable for every environment e.g. (dev, staging, live)
  • 6.
    Example Bootstrap <?php // Allenvironment configuration for this application is done from this file ini_set('display_errors', 0); ini_set('error_reporting', -1); date_default_timezone_set('UTC'); $rootPath = dirname(__FILE__); set_include_path(sprintf( '%s%s%s%s', $rootPath, PATH_SEPARATOR, $rootPath, '/third-party' )); require_once('MindFrame2/AutoLoad.php'); MindFrame2_AutoLoad::install();
  • 8.
    Don’t use globalconstants, variables, or registries  You can’t be certain that nobody else will ever use the same global constant name  Tracking down where a value is assigned to a global variable is usually very difficult  Global variables remove control of that variable from the current scope  Global registries are really just fancy global variables and the same problems apply
  • 9.
    Value Reference Rules Values should only be referenced in one of the following ways:  A direct reference in the current scope: $limit = $this->_objectPool->getConfig()- >getDefaultLimit();  A parameter: public function getUsers($limit) { return $this->_objectPool- >getMapper(‘User’)->fetchAll($limit); }
  • 10.
    Don’t use functionparameter defaults "There should be one-- and preferably only one -- obvious way to do it." - Tim Peters  Reduces understanding from the caller perspective  Often add unnecessary complexity  Represents the most common use-case  Other use-cases are often ignored entirely  Changing the default breaks all implementations  Adding parameters breaks default implementations
  • 11.
    Default parameter example classUser { public function __construct($name, $email, $status = self::STATUS_ACTIVE) { $this->_name = $name; $this->_email = $email; $this->_status = $status; } } function createUser($name, $email, $status) { $user = new User($name, email); return $this->_objectPoool->getMapper(‘User’)->create($user); }
  • 12.
    Use explicit operatorswhenever possible This was actual production code. if ($sincedate && (int)$sincedate) { $param["UpdatedSinceDate"] = date("Y-m-d H:i:s", strtotime(time() - ((int)$sincedate)*60*60); } elseif ($sincedate && strtotime($sincedate)) { $param["UpdatedSinceDate"] = date("Y-m-d H:i:s", strtotime($sincedate)); } elseif(!isset($param["UpdatedSinceDate"])) { exit ("Invalid param UpdatedSinceDate"); } Do you think this code worked as the author intended when $sincedate was set to '2011-05-31'?
  • 13.
    Use constants forpossible values Constants cannot be re-defined, so using a constant for configuration limits your code to only ever be able to run in one configuration in a given stack. // Bad: This code cannot execute differently in the same stack define(‘PDO_FETCH_MODE’, 2); $dbi->fetchAll(PDO_FETCH_MODE); // Better: The value can change but this literal value means nothing without // referencing the documentation $dbi->fetchAll(2); // Best: Implemented using a meaningful constant which represents a *possible* value $dbi->fetchAll(PDO::FETCH_ASSOC);
  • 14.
    Use wrappers forsuper-globals  $_REQUEST, $_GET, $_SESSION, and $_FILES are the most commonly used PHP super-globals but there are others  It’s easy to create some serious security problems  It’s best to use a wrapper for interacting with these variables
  • 15.
    Simulate strong/static types PHPis dynamically typed, so we cannot do true strong or static typing but we can put controls in place which provide the same protection. private function _buildSelectDatabaseTableSql($database_name, $table_name, array $select_data, array $order_by_columns, $offset, $limit) { MindFrame2_Core::assertArgumentIsNotBlank($database_name, 1, 'database_name'); MindFrame2_Core::assertArgumentIsNotBlank($table_name, 2, 'table_name'); MindFrame2_Core::assertArgumentIsInt($offset, 5, 'offset'); MindFrame2_Core::assertArgumentIsInt($limit, 6, 'limit'); $skel = "SELECTn %snFROMn %s.%sn%s%s%s%s;"; $sql = sprintf($skel, $this->_buildSelectTableSqlFieldClause($table_name), $this->getSharedModule()->escapeDbElementName($database_name), $this->getSharedModule()->escapeDbElementName($table_name), $this->_buildSelectTableSqlJoinClause( $this->getSharedModule()->getDatabase()->getName(), $table_name), $this->_buildSelectTableSqlWhereClause($table_name, $select_data), $this->_buildSelectTableSqlOrderByClause( $table_name, $order_by_columns), $this->_buildSelectTableSqlLimitClause($offset, $limit)); return $sql; }
  • 16.
    Example Typing Assertions publicstatic function assertArgumentIsInt($value, $position, $name) { if (!is_int($value)) { $skel = 'Expected integer value for argument #%d (%s), %s given'; $message = sprintf($skel, $position, $name, gettype($value)); throw new InvalidArgumentException($message); } } public static function assertArgumentIsNotBlank($value, $position, $name) { if (trim($value) === '') { $skel = 'Argument #%d (%s) cannot be blank'; $message = sprintf($skel, $position, $name); throw new InvalidArgumentException($message); } }
  • 17.
    Use PHPMD &CodeSniffer This is the ./bin/codecheck script in MindFrame2 #!/bin/bash WD="`/usr/bin/dirname $0`/../"; echo Changing to working directory: $WD; cd $WD; if [ -z $1 ]; then DIR='.'; else DIR=$1; fi phpmd --exclude coverage $DIR text codesize,unusedcode,naming,design phpcs --ignore=coverage --standard=`pwd`/Standards/MindFrame2 $DIR phpcs --ignore=coverage --report=source --standard=`pwd`/Standards/MindFrame2 $DIR
  • 18.
    Example PHPMD Output bryan@ubuntu-virtual~/Code/MindFrame2 [130] $ ./bin/codecheck Dbms/Dbi Changing to working directory: ./bin/../ /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:128 Avoid unused local variables such as '$partner'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:185 Avoid unused local variables such as '$data'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:199 Avoid unused local variables such as '$index'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Distributed.php:240 Avoid unused private methods such as '_buildReplicaDatabaseSuffix'. /home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php:25 This class has too many methods, consider refactoring it. /home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php:150 Avoid unused parameters such as '$options'.
  • 19.
    Example PHPCS Output FILE:/home/bryan/Code/MindFrame2/Dbms/Dbi/Single.php -------------------------------------------------------------------------------- FOUND 6 ERROR(S) AFFECTING 6 LINE(S) -------------------------------------------------------------------------------- 39 | ERROR | Public member variables ("_connection") are forbidden 62 | ERROR | Whitespace found at end of line 144 | ERROR | Whitespace found at end of line 160 | ERROR | Line exceeds maximum limit of 80 characters; contains 97 | | characters 195 | ERROR | Whitespace found at end of line 298 | ERROR | Line exceeds maximum limit of 80 characters; contains 81 | | characters --------------------------------------------------------------------------------
  • 20.
    Unit testing /TDD  Test critical functionality at a minimum  Add edge cases to regression tests as they are discovered  Test-driven development only works if the entire team is dedicated to it  I personally don’t trust code that isn’t regression-tested; even if it was written by me
  • 21.
    Continuous Integration  Automatedsystem for code analysis and regression testing  There are a lot of continuous integration systems for PHP  If your tests aren’t automated, very few people will know or care when they break
  • 22.
    Deployment  Manually copyingfiles is riddled with problems  Tagging releases and re-deploying is a complicated and time-consuming process  Deployment systems automate the build and deployment process
  • 23.
  • 24.
    If you’re interestedin an application security career, come talk with me.