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

Post on 04-Jul-2015

2.089 views 2 download

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

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 set the defaults for any PHP core functionality that your application is using.

These must be customizable for every environment

e.g. (dev, staging, live)

Absolute Minimum

Include paths

Error reporting

Time zones

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

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

"There should be one-- and preferably only one --obvious way to do it." - Tim Peters

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

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");

}

This was actual production code.

Do you think this code worked as the author intended when $sincedate was set to '2011-05-31'?

Use constants for possible values

// 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);

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.

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

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 = "SELECT\n %s\nFROM\n %s.%s\n%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;

}

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.

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

#!/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

This is the ./bin/codecheck script in MindFrame2

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.