diff --git a/libs/csrf/LoggerInterface.php b/libs/csrf/LoggerInterface.php new file mode 100644 index 0000000..498b4dd --- /dev/null +++ b/libs/csrf/LoggerInterface.php @@ -0,0 +1,26 @@ +path = $cfg['path']; + if (isset($cfg['domain'])) $this->domain = $cfg['domain']; + if (isset($cfg['secure'])) $this->secure = (bool) $cfg['secure']; + if (isset($cfg['expire']) && $cfg['expire']) $this->expire = (int)$cfg['expire']; + } + } + } +} \ No newline at end of file diff --git a/libs/csrf/csrfpDefaultLogger.php b/libs/csrf/csrfpDefaultLogger.php new file mode 100644 index 0000000..da017fe --- /dev/null +++ b/libs/csrf/csrfpDefaultLogger.php @@ -0,0 +1,88 @@ +logDirectory = __DIR__ . "/../" . $path; + + + //// If the relative log directory path does not + //// exist try as an absolute path + if (!is_dir($this->logDirectory)) { + $this->logDirectory = $path; + } + + if (!is_dir($this->logDirectory)) { + throw new logDirectoryNotFoundException("OWASP CSRFProtector: Log Directory Not Found!"); + } + } + + /** + * logging method + * + * Parameters: + * $message - the log message + * $context - context array + * + * Return: + * void + * + * Throws: + * logFileWriteError - if unable to log an attack + */ + public function log($message, $context = array()) { + // Append to the log file, or create it if it does not exist create + $logFile = fopen($this->logDirectory ."/" . date("m-20y") . ".log", "a+"); + + //throw exception if above fopen fails + if (!$logFile) { + throw new logFileWriteError("OWASP CSRFProtector: Unable to write to the log file"); + } + + $context['timestamp'] = time(); + $context['message'] = $message; + + //convert log array to JSON format to be logged + $context = json_encode($context) .PHP_EOL; + + //append log to the file + fwrite($logFile, $context); + + //close the file handler + fclose($logFile); + } + } +} \ No newline at end of file diff --git a/libs/csrf/csrfprotector.php b/libs/csrf/csrfprotector.php index 3ee1ddf..9d2792e 100755 --- a/libs/csrf/csrfprotector.php +++ b/libs/csrf/csrfprotector.php @@ -1,648 +1,583 @@ path = $cfg['path']; - if (isset($cfg['domain'])) $this->domain = $cfg['domain']; - if (isset($cfg['secure'])) $this->secure = (bool) $cfg['secure']; - if (isset($cfg['expire']) && $cfg['expire']) $this->expire = (int)$cfg['expire']; - } - } - } - - class csrfProtector - { - /* - * Variable: $isSameOrigin - * flag for cross origin/same origin request - * @var bool - */ - private static $isSameOrigin = true; - - /* - * Variable: $isValidHTML - * flag to check if output file is a valid HTML or not - * @var bool - */ - private static $isValidHTML = false; - - /** - * Variable: $cookieConfig - * Array of parameters for the setcookie method - * @var array - */ - private static $cookieConfig = null; - - /** - * Variable: $tokenHeaderKey - * Key value in header array, which contain the token - * @var string - */ - private static $tokenHeaderKey = null; - - /* - * Variable: $requestType - * Variable to store whether request type is post or get - * @var string - */ - protected static $requestType = "GET"; - - /* - * Variable: $config - * config file for CSRFProtector - * @var int Array, length = 6 - * Property: #1: failedAuthAction (int) => action to be taken in case autherisation fails - * Property: #2: logDirectory (string) => directory in which log will be saved - * Property: #3: customErrorMessage (string) => custom error message to be sent in case - * of failed authentication - * Property: #4: jsFile (string) => location of the CSRFProtector js file - * Property: #5: tokenLength (int) => default length of hash - * Property: #6: disabledJavascriptMessage (string) => error message if client's js is disabled - */ - public static $config = array(); - - /* - * Variable: $requiredConfigurations - * Contains list of those parameters that are required to be there - * in config file for csrfp to work - */ - public static $requiredConfigurations = array('logDirectory', 'failedAuthAction', 'jsUrl', 'tokenLength'); - - /* - * Function: init - * - * function to initialise the csrfProtector work flow - * - * Parameters: - * $length - length of CSRF_AUTH_TOKEN to be generated - * $action - int array, for different actions to be taken in case of failed validation - * - * Returns: - * void - * - * Throws: - * configFileNotFoundException - when configuration file is not found - * incompleteConfigurationException - when all required fields in config - * file are not available - * - */ - public static function init($length = null, $action = null) - { - /* - * Check if init has already been called. - */ - if (count(self::$config) > 0) { - throw new alreadyInitializedException("OWASP CSRFProtector: library was already initialized."); - } - - /* - * if mod_csrfp already enabled, no verification, no filtering - * Already done by mod_csrfp - */ - if (getenv('mod_csrfp_enabled')) - return; - - //start session in case its not - if (session_id() == '') - session_start(); - - /* - * load configuration file and properties - * Check locally for a config.php then check for - * a config/csrf_config.php file in the root folder - * for composer installations - */ - $standard_config_location = __DIR__ ."/../config.php"; - $composer_config_location = __DIR__ ."/../../../../../config/csrf_config.php"; - - if (file_exists($standard_config_location)) { - self::$config = include($standard_config_location); - } elseif(file_exists($composer_config_location)) { - self::$config = include($composer_config_location); - } else { - throw new configFileNotFoundException("OWASP CSRFProtector: configuration file not found for CSRFProtector!"); - } - - //overriding length property if passed in parameters - if ($length != null) - self::$config['tokenLength'] = intval($length); - - //action that is needed to be taken in case of failed authorisation - if ($action != null) - self::$config['failedAuthAction'] = $action; - - if (self::$config['CSRFP_TOKEN'] == '') - self::$config['CSRFP_TOKEN'] = CSRFP_TOKEN; - - self::$tokenHeaderKey = 'HTTP_' .strtoupper(self::$config['CSRFP_TOKEN']); - self::$tokenHeaderKey = str_replace('-', '_', self::$tokenHeaderKey); - - // load parameters for setcookie method - if (!isset(self::$config['cookieConfig'])) - self::$config['cookieConfig'] = array(); - self::$cookieConfig = new cookieConfig(self::$config['cookieConfig']); - - // Validate the config if everything is filled out - $missingConfiguration = []; - foreach (self::$requiredConfigurations as $value) { - if (!isset(self::$config[$value]) || self::$config[$value] === '') { - $missingConfiguration[] = $value; - } - } - - if ($missingConfiguration) { - throw new incompleteConfigurationException( - 'OWASP CSRFProtector: Incomplete configuration file: missing ' . - implode(', ', $missingConfiguration) . ' value(s)'); - } - - // Authorise the incoming request - self::authorizePost(); - - // Initialize output buffering handler - if (!defined('__TESTING_CSRFP__')) - ob_start('csrfProtector::ob_handler'); - - if (!isset($_COOKIE[self::$config['CSRFP_TOKEN']]) - || !isset($_SESSION[self::$config['CSRFP_TOKEN']]) - || !is_array($_SESSION[self::$config['CSRFP_TOKEN']]) - || !in_array($_COOKIE[self::$config['CSRFP_TOKEN']], - $_SESSION[self::$config['CSRFP_TOKEN']])) - self::refreshToken(); - - // Set protected by CSRF Protector header - header('X-CSRF-Protection: OWASP CSRFP 1.0.0'); - } - - /* - * Function: authorizePost - * function to authorise incoming post requests - * - * Parameters: - * void - * - * Returns: - * void - * - * Throws: - * logDirectoryNotFoundException - if log directory is not found - */ - public static function authorizePost() - { - //#todo this method is valid for same origin request only, - //enable it for cross origin also sometime - //for cross origin the functionality is different - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - - //set request type to POST - self::$requestType = "POST"; - - // look for token in payload else from header - $token = self::getTokenFromRequest(); - - //currently for same origin only - if (!($token && isset($_SESSION[self::$config['CSRFP_TOKEN']]) - && (self::isValidToken($token)))) { - - //action in case of failed validation - self::failedValidationAction(); - } else { - self::refreshToken(); //refresh token for successful validation - } - } else if (!static::isURLallowed()) { - //currently for same origin only - if (!(isset($_GET[self::$config['CSRFP_TOKEN']]) - && isset($_SESSION[self::$config['CSRFP_TOKEN']]) - && (self::isValidToken($_GET[self::$config['CSRFP_TOKEN']])) - )) { - - //action in case of failed validation - self::failedValidationAction(); - } else { - self::refreshToken(); //refresh token for successful validation - } - } - } - - /* - * Function: getTokenFromRequest - * function to get token in case of POST request - * - * Parameters: - * void - * - * Returns: - * any (string / bool) - token retrieved from header or form payload - */ - private static function getTokenFromRequest() { - // look for in $_POST, then header - if (isset($_POST[self::$config['CSRFP_TOKEN']])) { - return $_POST[self::$config['CSRFP_TOKEN']]; - } - - if (function_exists('apache_request_headers')) { - $apacheRequestHeaders = apache_request_headers(); - if (isset($apacheRequestHeaders[self::$config['CSRFP_TOKEN']])) { - return $apacheRequestHeaders[self::$config['CSRFP_TOKEN']]; - } - } - - if (self::$tokenHeaderKey === null) return false; - if (isset($_SERVER[self::$tokenHeaderKey])) { - return $_SERVER[self::$tokenHeaderKey]; - } - - return false; - } - - /* - * Function: isValidToken - * function to check the validity of token in session array - * Function also clears all tokens older than latest one - * - * Parameters: - * $token - the token sent with GET or POST payload - * - * Returns: - * bool - true if its valid else false - */ - private static function isValidToken($token) { - if (!isset($_SESSION[self::$config['CSRFP_TOKEN']])) return false; - if (!is_array($_SESSION[self::$config['CSRFP_TOKEN']])) return false; - foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $key => $value) { - if ($value == $token) { - - // Clear all older tokens assuming they have been consumed - foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $_key => $_value) { - if ($_value == $token) break; - array_shift($_SESSION[self::$config['CSRFP_TOKEN']]); - } - return true; - } - } - - return false; - } - - /* - * Function: failedValidationAction - * function to be called in case of failed validation - * performs logging and take appropriate action - * - * Parameters: - * void - * - * Returns: - * void - */ - private static function failedValidationAction() - { - //call the logging function - static::logCSRFattack(); - - //#todo: ask mentors if $failedAuthAction is better as an int or string - //default case is case 0 - switch (self::$config['failedAuthAction'][self::$requestType]) { - case 0: - //send 403 header - header('HTTP/1.0 403 Forbidden'); - exit("

403 Access Forbidden by CSRFProtector!

"); - break; - case 1: - //unset the query parameters and forward - if (self::$requestType === 'GET') { - $_GET = array(); - } else { - $_POST = array(); - } - break; - case 2: - //redirect to custom error page - $location = self::$config['errorRedirectionPage']; - header("location: $location"); - exit(self::$config['customErrorMessage']); - break; - case 3: - //send custom error message - exit(self::$config['customErrorMessage']); - break; - case 4: - //send 500 header -- internal server error - header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); - exit("

500 Internal Server Error!

"); - break; - default: - //unset the query parameters and forward - if (self::$requestType === 'GET') { - $_GET = array(); - } else { - $_POST = array(); - } - break; - } - } - - /* - * Function: refreshToken - * Function to set auth cookie - * - * Parameters: - * void - * - * Returns: - * void - */ - public static function refreshToken() - { - $token = self::generateAuthToken(); - - if (!isset($_SESSION[self::$config['CSRFP_TOKEN']]) - || !is_array($_SESSION[self::$config['CSRFP_TOKEN']])) - $_SESSION[self::$config['CSRFP_TOKEN']] = array(); - - // set token to session for server side validation - array_push($_SESSION[self::$config['CSRFP_TOKEN']], $token); - - // set token to cookie for client side processing - if (self::$cookieConfig === null) { - if (!isset(self::$config['cookieConfig'])) - self::$config['cookieConfig'] = array(); - self::$cookieConfig = new cookieConfig(self::$config['cookieConfig']); - } - - setcookie( - self::$config['CSRFP_TOKEN'], - $token, - time() + self::$cookieConfig->expire, - self::$cookieConfig->path, - self::$cookieConfig->domain, - (bool) self::$cookieConfig->secure); - } - - /* - * Function: generateAuthToken - * function to generate random hash of length as given in parameter - * max length = 128 - * - * Parameters: - * length to hash required, int - * - * Returns: - * string, token - */ - public static function generateAuthToken() - { - // todo - make this a member method / configurable - $randLength = 64; - - //if config tokenLength value is 0 or some non int - if (intval(self::$config['tokenLength']) == 0) { - self::$config['tokenLength'] = 32; //set as default - } - - //#todo - if $length > 128 throw exception - - if (function_exists("random_bytes")) { - $token = bin2hex(random_bytes($randLength)); - } elseif (function_exists("openssl_random_pseudo_bytes")) { - $token = bin2hex(openssl_random_pseudo_bytes($randLength)); - } else { - $token = ''; - for ($i = 0; $i < 128; ++$i) { - $r = mt_rand (0, 35); - if ($r < 26) { - $c = chr(ord('a') + $r); - } else { - $c = chr(ord('0') + $r - 26); - } - $token .= $c; - } - } - return substr($token, 0, self::$config['tokenLength']); - } - - /* - * Function: ob_handler - * Rewrites
on the fly to add CSRF tokens to them. This can also - * inject our JavaScript library. - * - * Parameters: - * $buffer - output buffer to which all output are stored - * $flag - INT - * - * Return: - * string, complete output buffer - */ - public static function ob_handler($buffer, $flags) - { - // Even though the user told us to rewrite, we should do a quick heuristic - // to check if the page is *actually* HTML. We don't begin rewriting until - // we hit the first message to outgoing HTML output, - //informing the user to enable js for CSRFProtector to work - //best section to add, after tag - $buffer = preg_replace("/]*>/", "$0 ", $buffer); - - $hiddenInput = '' .PHP_EOL; - - $hiddenInput .= ''; - - //implant hidden fields with check url information for reading in javascript - $buffer = str_ireplace('', $hiddenInput . '', $buffer); - - if (self::$config['jsUrl']) { - //implant the CSRFGuard js file to outgoing script - $script = ''; - $buffer = str_ireplace('', $script . PHP_EOL . '', $buffer, $count); - - // Add the script to the end if the body tag was not closed - if (!$count) - $buffer .= $script; - } - - return $buffer; - } - - /* - * Function: logCSRFattack - * Function to log CSRF Attack - * - * Parameters: - * void - * - * Returns: - * void - * - * Throws: - * logFileWriteError - if unable to log an attack - */ - protected static function logCSRFattack() - { - $logDirectory = __DIR__ . "/../" . self::$config['logDirectory']; - - // If the relative log directory path does not exist try as an absolute path - if (!is_dir($logDirectory)) { - $logDirectory = self::$config['logDirectory']; - } - - if (!is_dir($logDirectory)) - throw new logDirectoryNotFoundException("OWASP CSRFProtector: Log Directory Not Found!"); - - // Append to the log file, or create it if it does not exist create - $logFile = fopen("$logDirectory/" . date("m-20y") . ".log", "a+"); - - //throw exception if above fopen fails - if (!$logFile) - throw new logFileWriteError("OWASP CSRFProtector: Unable to write to the log file"); - - //miniature version of the log - $log = array(); - $log['timestamp'] = time(); - $log['HOST'] = $_SERVER['HTTP_HOST']; - $log['REQUEST_URI'] = $_SERVER['REQUEST_URI']; - $log['requestType'] = self::$requestType; - - if (self::$requestType === "GET") - $log['query'] = $_GET; - else - $log['query'] = $_POST; - - $log['cookie'] = $_COOKIE; - - //convert log array to JSON format to be logged - $log = json_encode($log) .PHP_EOL; - - //append log to the file - fwrite($logFile, $log); - - //close the file handler - fclose($logFile); - } - - /* - * Function: getCurrentUrl - * Function to return current url of executing page - * - * Parameters: - * void - * - * Returns: - * string - current url - */ - private static function getCurrentUrl() - { - $request_scheme = 'https'; - - if (isset($_SERVER['REQUEST_SCHEME'])) { - $request_scheme = $_SERVER['REQUEST_SCHEME']; - } else { - if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { - $request_scheme = 'https'; - } else { - $request_scheme = 'http'; - } - } - - return $request_scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; - } - - /* - * Function: isURLallowed - * Function to check if a url matches for any urls - * Listed in config file - * - * Parameters: - * void - * - * Returns: - * boolean - true is url need no validation, false if validation needed - */ - public static function isURLallowed() { - foreach (self::$config['verifyGetFor'] as $key => $value) { - $value = str_replace(array('/','*'), array('\/','(.*)'), $value); - preg_match('/' .$value .'/', self::getCurrentUrl(), $output); - if (count($output) > 0) - return false; - } - return true; - } - }; + define('__CSRF_PROTECTOR__', true); // to avoid multiple declaration errors + + // name of HTTP POST variable for authentication + define("CSRFP_TOKEN","csrfp_token"); + + // We insert token name and list of url patterns for which + // GET requests are validated against CSRF as hidden input fields + // these are the names of the input fields + define("CSRFP_FIELD_TOKEN_NAME", "csrfp_hidden_data_token"); + define("CSRFP_FIELD_URLS", "csrfp_hidden_data_urls"); + + // Include the csrfpCookieConfig class + + /** + * child exception classes + */ + class configFileNotFoundException extends \exception {}; + class jsFileNotFoundException extends \exception {}; + class baseJSFileNotFoundExceptio extends \exception {}; + class incompleteConfigurationException extends \exception {}; + class alreadyInitializedException extends \exception {}; + + class csrfProtector + { + /* + * Variable: $isSameOrigin + * flag for cross origin/same origin request + * @var bool + */ + private static $isSameOrigin = true; + + /* + * Variable: $isValidHTML + * flag to check if output file is a valid HTML or not + * @var bool + */ + private static $isValidHTML = false; + + /** + * Variable: $cookieConfig + * Array of parameters for the setcookie method + * @var array + */ + private static $cookieConfig = null; + + /** + * Variable: $logger + * Logger class object + * @var LoggerInterface + */ + private static $logger = null; + + /** + * Variable: $tokenHeaderKey + * Key value in header array, which contain the token + * @var string + */ + private static $tokenHeaderKey = null; + + /* + * Variable: $requestType + * Variable to store whether request type is post or get + * @var string + */ + protected static $requestType = "GET"; + + /* + * Variable: $config + * config file for CSRFProtector + * @var int Array, length = 6 + * Property: #1: failedAuthAction (int) => action to be taken in case autherisation fails + * Property: #2: logDirectory (string) => directory in which log will be saved + * Property: #3: customErrorMessage (string) => custom error message to be sent in case + * of failed authentication + * Property: #4: jsFile (string) => location of the CSRFProtector js file + * Property: #5: tokenLength (int) => default length of hash + * Property: #6: disabledJavascriptMessage (string) => error message if client's js is disabled + */ + public static $config = array(); + + /* + * Variable: $requiredConfigurations + * Contains list of those parameters that are required to be there + * in config file for csrfp to work + */ + public static $requiredConfigurations = array('logDirectory', 'failedAuthAction', 'jsUrl', 'tokenLength'); + + /* + * Function: init + * + * function to initialise the csrfProtector work flow + * + * Parameters: + * $length - length of CSRF_AUTH_TOKEN to be generated + * $action - int array, for different actions to be taken in case of failed validation + * + * Returns: + * void + * + * Throws: + * configFileNotFoundException - when configuration file is not found + * incompleteConfigurationException - when all required fields in config + * file are not available + * + */ + public static function init($length = null, $action = null, $logger = null) + { + /* + * Check if init has already been called. + */ + if (count(self::$config) > 0) { + throw new alreadyInitializedException("OWASP CSRFProtector: library was already initialized."); + } + + /* + * if mod_csrfp already enabled, no verification, no filtering + * Already done by mod_csrfp + */ + if (getenv('mod_csrfp_enabled')) + return; + + // start session in case its not, and unit test is not going on + if (session_id() == '' && !defined('__CSRFP_UNIT_TEST__')) + session_start(); + + /* + * load configuration file and properties + * Check locally for a config.php then check for + * a config/csrf_config.php file in the root folder + * for composer installations + */ + $standard_config_location = __DIR__ ."/../config.php"; + $composer_config_location = __DIR__ ."/../../../../../config/csrf_config.php"; + + if (file_exists($standard_config_location)) { + self::$config = include($standard_config_location); + } elseif(file_exists($composer_config_location)) { + self::$config = include($composer_config_location); + } else { + throw new configFileNotFoundException("OWASP CSRFProtector: configuration file not found for CSRFProtector!"); + } + + //overriding length property if passed in parameters + if ($length != null) + self::$config['tokenLength'] = intval($length); + + //action that is needed to be taken in case of failed authorisation + if ($action != null) + self::$config['failedAuthAction'] = $action; + + if (self::$config['CSRFP_TOKEN'] == '') + self::$config['CSRFP_TOKEN'] = CSRFP_TOKEN; + + self::$tokenHeaderKey = 'HTTP_' .strtoupper(self::$config['CSRFP_TOKEN']); + self::$tokenHeaderKey = str_replace('-', '_', self::$tokenHeaderKey); + + // load parameters for setcookie method + if (!isset(self::$config['cookieConfig'])) + self::$config['cookieConfig'] = array(); + self::$cookieConfig = new csrfpCookieConfig(self::$config['cookieConfig']); + + // Validate the config if everything is filled out + $missingConfiguration = []; + foreach (self::$requiredConfigurations as $value) { + if (!isset(self::$config[$value]) || self::$config[$value] === '') { + $missingConfiguration[] = $value; + } + } + + if ($missingConfiguration) { + throw new incompleteConfigurationException( + 'OWASP CSRFProtector: Incomplete configuration file: missing ' . + implode(', ', $missingConfiguration) . ' value(s)'); + } + + // iniialize the logger class + if ($logger !== null) { + self::$logger = $logger; + } else { + self::$logger = new csrfpDefaultLogger(self::$config['logDirectory']); + } + + // Authorise the incoming request + self::authorizePost(); + + // Initialize output buffering handler + if (!defined('__TESTING_CSRFP__')) + ob_start('csrfProtector::ob_handler'); + + if (!isset($_COOKIE[self::$config['CSRFP_TOKEN']]) + || !isset($_SESSION[self::$config['CSRFP_TOKEN']]) + || !is_array($_SESSION[self::$config['CSRFP_TOKEN']]) + || !in_array($_COOKIE[self::$config['CSRFP_TOKEN']], + $_SESSION[self::$config['CSRFP_TOKEN']])) + self::refreshToken(); + } + + /* + * Function: authorizePost + * function to authorise incoming post requests + * + * Parameters: + * void + * + * Returns: + * void + * + * Throws: + * logDirectoryNotFoundException - if log directory is not found + */ + public static function authorizePost() + { + //#todo this method is valid for same origin request only, + //enable it for cross origin also sometime + //for cross origin the functionality is different + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + //set request type to POST + self::$requestType = "POST"; + + // look for token in payload else from header + $token = self::getTokenFromRequest(); + + //currently for same origin only + if (!($token && isset($_SESSION[self::$config['CSRFP_TOKEN']]) + && (self::isValidToken($token)))) { + + //action in case of failed validation + self::failedValidationAction(); + } else { + self::refreshToken(); //refresh token for successful validation + } + } else if (!static::isURLallowed()) { + //currently for same origin only + if (!(isset($_GET[self::$config['CSRFP_TOKEN']]) + && isset($_SESSION[self::$config['CSRFP_TOKEN']]) + && (self::isValidToken($_GET[self::$config['CSRFP_TOKEN']])) + )) { + + //action in case of failed validation + self::failedValidationAction(); + } else { + self::refreshToken(); //refresh token for successful validation + } + } + } + + /* + * Function: getTokenFromRequest + * function to get token in case of POST request + * + * Parameters: + * void + * + * Returns: + * any (string / bool) - token retrieved from header or form payload + */ + private static function getTokenFromRequest() { + // look for in $_POST, then header + if (isset($_POST[self::$config['CSRFP_TOKEN']])) { + return $_POST[self::$config['CSRFP_TOKEN']]; + } + + if (function_exists('apache_request_headers')) { + $apacheRequestHeaders = apache_request_headers(); + if (isset($apacheRequestHeaders[self::$config['CSRFP_TOKEN']])) { + return $apacheRequestHeaders[self::$config['CSRFP_TOKEN']]; + } + } + + if (self::$tokenHeaderKey === null) return false; + if (isset($_SERVER[self::$tokenHeaderKey])) { + return $_SERVER[self::$tokenHeaderKey]; + } + + return false; + } + + /* + * Function: isValidToken + * function to check the validity of token in session array + * Function also clears all tokens older than latest one + * + * Parameters: + * $token - the token sent with GET or POST payload + * + * Returns: + * bool - true if its valid else false + */ + private static function isValidToken($token) { + if (!isset($_SESSION[self::$config['CSRFP_TOKEN']])) return false; + if (!is_array($_SESSION[self::$config['CSRFP_TOKEN']])) return false; + foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $key => $value) { + if ($value == $token) { + + // Clear all older tokens assuming they have been consumed + foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $_key => $_value) { + if ($_value == $token) break; + array_shift($_SESSION[self::$config['CSRFP_TOKEN']]); + } + return true; + } + } + + return false; + } + + /* + * Function: failedValidationAction + * function to be called in case of failed validation + * performs logging and take appropriate action + * + * Parameters: + * void + * + * Returns: + * void + */ + private static function failedValidationAction() + { + //call the logging function + static::logCSRFattack(); + + //#todo: ask mentors if $failedAuthAction is better as an int or string + //default case is case 0 + switch (self::$config['failedAuthAction'][self::$requestType]) { + case csrfpAction::ForbiddenResponseAction: + //send 403 header + header('HTTP/1.0 403 Forbidden'); + exit("

403 Access Forbidden by CSRFProtector!

"); + break; + case csrfpAction::ClearParametersAction: + //unset the query parameters and forward + if (self::$requestType === 'GET') { + $_GET = array(); + } else { + $_POST = array(); + } + break; + case csrfpAction::RedirectAction: + //redirect to custom error page + $location = self::$config['errorRedirectionPage']; + header("location: $location"); + exit(self::$config['customErrorMessage']); + break; + case csrfpAction::CustomErrorMessageAction: + //send custom error message + exit(self::$config['customErrorMessage']); + break; + case csrfpAction::InternalServerErrorResponseAction: + //send 500 header -- internal server error + header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); + exit("

500 Internal Server Error!

"); + break; + default: + //unset the query parameters and forward + if (self::$requestType === 'GET') { + $_GET = array(); + } else { + $_POST = array(); + } + break; + } + } + + /* + * Function: refreshToken + * Function to set auth cookie + * + * Parameters: + * void + * + * Returns: + * void + */ + public static function refreshToken() + { + $token = self::generateAuthToken(); + + if (!isset($_SESSION[self::$config['CSRFP_TOKEN']]) + || !is_array($_SESSION[self::$config['CSRFP_TOKEN']])) + $_SESSION[self::$config['CSRFP_TOKEN']] = array(); + + // set token to session for server side validation + array_push($_SESSION[self::$config['CSRFP_TOKEN']], $token); + + // set token to cookie for client side processing + if (self::$cookieConfig === null) { + if (!isset(self::$config['cookieConfig'])) + self::$config['cookieConfig'] = array(); + self::$cookieConfig = new csrfpCookieConfig(self::$config['cookieConfig']); + } + + setcookie( + self::$config['CSRFP_TOKEN'], + $token, + time() + self::$cookieConfig->expire, + self::$cookieConfig->path, + self::$cookieConfig->domain, + (bool) self::$cookieConfig->secure); + } + + /* + * Function: generateAuthToken + * function to generate random hash of length as given in parameter + * max length = 128 + * + * Parameters: + * length to hash required, int + * + * Returns: + * string, token + */ + public static function generateAuthToken() + { + // todo - make this a member method / configurable + $randLength = 64; + + //if config tokenLength value is 0 or some non int + if (intval(self::$config['tokenLength']) == 0) { + self::$config['tokenLength'] = 32; //set as default + } + + //#todo - if $length > 128 throw exception + + if (function_exists("random_bytes")) { + $token = bin2hex(random_bytes($randLength)); + } elseif (function_exists("openssl_random_pseudo_bytes")) { + $token = bin2hex(openssl_random_pseudo_bytes($randLength)); + } else { + $token = ''; + for ($i = 0; $i < 128; ++$i) { + $r = mt_rand (0, 35); + if ($r < 26) { + $c = chr(ord('a') + $r); + } else { + $c = chr(ord('0') + $r - 26); + } + $token .= $c; + } + } + return substr($token, 0, self::$config['tokenLength']); + } + + /* + * Function: ob_handler + * Rewrites on the fly to add CSRF tokens to them. This can also + * inject our JavaScript library. + * + * Parameters: + * $buffer - output buffer to which all output are stored + * $flag - INT + * + * Return: + * string, complete output buffer + */ + public static function ob_handler($buffer, $flags) + { + // Even though the user told us to rewrite, we should do a quick heuristic + // to check if the page is *actually* HTML. We don't begin rewriting until + // we hit the first message to outgoing HTML output, + //informing the user to enable js for CSRFProtector to work + //best section to add, after tag + $buffer = preg_replace("/]*>/", "$0 ", $buffer); + + $hiddenInput = '' .PHP_EOL; + + $hiddenInput .= ''; + + //implant hidden fields with check url information for reading in javascript + $buffer = str_ireplace('', $hiddenInput . '', $buffer); + + if (self::$config['jsUrl']) { + //implant the CSRFGuard js file to outgoing script + $script = ''; + $buffer = str_ireplace('', $script . PHP_EOL . '', $buffer, $count); + + // Add the script to the end if the body tag was not closed + if (!$count) + $buffer .= $script; + } + + return $buffer; + } + + /* + * Function: logCSRFattack + * Function to log CSRF Attack + * + * Parameters: + * void + * + * Returns: + * void + * + * Throws: + * logFileWriteError - if unable to log an attack + */ + protected static function logCSRFattack() + { + //miniature version of the log + $context = array(); + $context['HOST'] = $_SERVER['HTTP_HOST']; + $context['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $context['requestType'] = self::$requestType; + $context['cookie'] = $_COOKIE; + + self::$logger->log("OWASP CSRF PROTECTOR VALIDATION FAILURE", $context); + } + + /* + * Function: getCurrentUrl + * Function to return current url of executing page + * + * Parameters: + * void + * + * Returns: + * string - current url + */ + private static function getCurrentUrl() + { + $request_scheme = 'https'; + + if (isset($_SERVER['REQUEST_SCHEME'])) { + $request_scheme = $_SERVER['REQUEST_SCHEME']; + } else { + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + $request_scheme = 'https'; + } else { + $request_scheme = 'http'; + } + } + + return $request_scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; + } + + /* + * Function: isURLallowed + * Function to check if a url matches for any urls + * Listed in config file + * + * Parameters: + * void + * + * Returns: + * boolean - true is url need no validation, false if validation needed + */ + public static function isURLallowed() { + foreach (self::$config['verifyGetFor'] as $key => $value) { + $value = str_replace(array('/','*'), array('\/','(.*)'), $value); + preg_match('/' .$value .'/', self::getCurrentUrl(), $output); + if (count($output) > 0) + return false; + } + return true; + } + }; } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 34814a2..961ed2b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,6 +6,7 @@ ./test/csrfprotector_test.php + ./test/csrfprotector_test_customlogger.php diff --git a/test/csrfprotector_test.php b/test/csrfprotector_test.php index c72698b..6c864cb 100644 --- a/test/csrfprotector_test.php +++ b/test/csrfprotector_test.php @@ -1,6 +1,7 @@ = 7 && !class_exists('\PHPUnit_Framework_TestCase', true)) { class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); @@ -117,7 +118,7 @@ public function setUp() $data = file_get_contents(__DIR__ .'/config.test.php'); file_put_contents(__DIR__ .'/../libs/config.php', $data); - if (!defined('__TESTING_CSRFP__')) define('__TESTING_CSRFP__', true); + if (!defined('__CSRFP_UNIT_TEST__')) define('__CSRFP_UNIT_TEST__', true); } /** @@ -159,29 +160,29 @@ public function testCookieConfigClass() { ); // simple test - $cookieConfig = new cookieConfig($cfg); + $cookieConfig = new csrfpCookieConfig($cfg); $this->assertEquals("abcd", $cookieConfig->path); $this->assertEquals("abcd", $cookieConfig->domain); $this->assertEquals(true, $cookieConfig->secure); $this->assertEquals(600, $cookieConfig->expire); // default value test - $cookieConfig = new cookieConfig(array()); + $cookieConfig = new csrfpCookieConfig(array()); $this->assertEquals('', $cookieConfig->path); $this->assertEquals('', $cookieConfig->domain); $this->assertEquals(false, $cookieConfig->secure); $this->assertEquals(1800, $cookieConfig->expire); // secure as string - $cookieConfig = new cookieConfig(array('secure' => 'true')); + $cookieConfig = new csrfpCookieConfig(array('secure' => 'true')); $this->assertEquals(true, $cookieConfig->secure); - $cookieConfig = new cookieConfig(array('secure' => 'false')); + $cookieConfig = new csrfpCookieConfig(array('secure' => 'false')); $this->assertEquals(true, $cookieConfig->secure); // expire as string - $cookieConfig = new cookieConfig(array('expire' => '600')); + $cookieConfig = new csrfpCookieConfig(array('expire' => '600')); $this->assertEquals(600, $cookieConfig->expire); - $cookieConfig = new cookieConfig(array('expire' => '')); + $cookieConfig = new csrfpCookieConfig(array('expire' => '')); $this->assertEquals(1800, $cookieConfig->expire); } @@ -202,12 +203,12 @@ public function testSecureCookie() $property->setAccessible(true); // change value to false - $property->setValue($csrfp, new cookieConfig(array('secure' => false))); + $property->setValue($csrfp, new csrfpCookieConfig(array('secure' => false))); csrfprotector::refreshToken(); $this->assertNotRegExp('/; secure/', csrfp_wrapper::getHeaderValue('Set-Cookie')); // change value to true - $property->setValue($csrfp, new cookieConfig(array('secure' => true))); + $property->setValue($csrfp, new csrfpCookieConfig(array('secure' => true))); csrfprotector::refreshToken(); $this->assertRegExp('/; secure/', csrfp_wrapper::getHeaderValue('Set-Cookie')); } @@ -229,7 +230,7 @@ public function testCookieExpireTime() $property->setAccessible(true); // change value to 600 - $property->setValue($csrfp, new cookieConfig(array('expire' => 600))); + $property->setValue($csrfp, new csrfpCookieConfig(array('expire' => 600))); csrfprotector::refreshToken(); // Check the expire date to the nearest minute in case the seconds does not match during test execution $this->assertRegExp('/; expires=' . date('D, d-M-Y H:i', time() + 600) . ':\d\d GMT;?/', csrfp_wrapper::getHeaderValue('Set-Cookie')); @@ -265,8 +266,13 @@ public function testAuthorisePost_failedAction_1() public function testAuthorisePost_failedAction_2() { $_SERVER['REQUEST_METHOD'] = 'POST'; + $csrfp = new csrfProtector; + $reflection = new \ReflectionClass(get_class($csrfp)); + $property = $reflection->getProperty('logger'); + $property->setAccessible(true); + // change value to false + $property->setValue($csrfp, new csrfpDefaultLogger('../log')); - csrfprotector::$config['logDirectory'] = '../log'; csrfprotector::$config['verifyGetFor'] = array('http://test/index*'); csrfprotector::$config['failedAuthAction']['POST'] = 1; csrfprotector::$config['failedAuthAction']['GET'] = 1; @@ -355,8 +361,13 @@ public function testAuthorisePost_failedAction_5() public function testAuthorisePost_failedAction_6() { $_SERVER['REQUEST_METHOD'] = 'POST'; + $csrfp = new csrfProtector; + $reflection = new \ReflectionClass(get_class($csrfp)); + $property = $reflection->getProperty('logger'); + $property->setAccessible(true); + // change value to false + $property->setValue($csrfp, new csrfpDefaultLogger('../log')); - csrfprotector::$config['logDirectory'] = '../log'; csrfprotector::$config['verifyGetFor'] = array('http://test/index*'); csrfprotector::$config['failedAuthAction']['POST'] = 10; csrfprotector::$config['failedAuthAction']['GET'] = 10; @@ -584,14 +595,7 @@ public function testgetCurrentUrl() */ public function testlogCSRFattack_logDirException() { - $_SERVER['REQUEST_METHOD'] = 'POST'; - csrfprotector::$config['logDirectory'] = 'unknown_location'; - - $stub = new ReflectionClass('csrfprotector'); - $method = $stub->getMethod('logCSRFattack'); - $method->setAccessible(true); - - $method->invoke(null); + new csrfpDefaultLogger('unknown_location'); } /** @@ -600,10 +604,6 @@ public function testlogCSRFattack_logDirException() */ public function testlogCSRFattack_logFileError() { - $stub = new ReflectionClass('csrfprotector'); - $method = $stub->getMethod('logCSRFattack'); - $method->setAccessible(true); - // Setting error reporting to E_ERROR and creating a directory with the same name as the log file will force // fopen to return FALSE $errorReportingLevel = error_reporting(E_ERROR); @@ -612,7 +612,8 @@ public function testlogCSRFattack_logFileError() if (!is_dir($logFilename)) mkdir($logFilename, 0777, true); try { - $method->invoke(null); + $logger = new csrfpDefaultLogger($this->logDir); + $logger->log("test"); } catch (Exception $e) { // Reset the error reporting level error_reporting($errorReportingLevel); @@ -625,11 +626,21 @@ public function testlogCSRFattack_logFileError() */ public function testlogCSRFattack() { + //// TODO: create log directory if not exists + if (!is_dir($this->logDir)) mkdir($this->logDir); + + $csrfp = new csrfProtector; + $reflection = new \ReflectionClass(get_class($csrfp)); + $property = $reflection->getProperty('logger'); + $property->setAccessible(true); + // change value to false + $property->setValue($csrfp, new csrfpDefaultLogger($this->logDir)); + $stub = new ReflectionClass('csrfprotector'); $method = $stub->getMethod('logCSRFattack'); $method->setAccessible(true); - if (!is_dir($this->logDir)) mkdir($this->logDir); + $method->invoke(null); $this->assertFileExists($this->logDir . "/" . date("m-20y") . ".log"); } @@ -639,11 +650,20 @@ public function testlogCSRFattack() */ public function testlogCSRFattack_withAbsoluteLogDirectory() { + //// TODO: create log directory if not exists + if (!is_dir($this->logDir)) mkdir($this->logDir); + + $csrfp = new csrfProtector; + $reflection = new \ReflectionClass(get_class($csrfp)); + $property = $reflection->getProperty('logger'); + $property->setAccessible(true); + // change value to false + $property->setValue($csrfp, new csrfpDefaultLogger($this->logDir)); + $stub = new ReflectionClass('csrfprotector'); $method = $stub->getMethod('logCSRFattack'); $method->setAccessible(true); - if (!is_dir($this->logDir)) mkdir($this->logDir); csrfprotector::$config['logDirectory'] = realpath($this->logDir); diff --git a/test/csrfprotector_test_customlogger.php b/test/csrfprotector_test_customlogger.php new file mode 100644 index 0000000..e9080bf --- /dev/null +++ b/test/csrfprotector_test_customlogger.php @@ -0,0 +1,74 @@ += 7 && !class_exists('\PHPUnit_Framework_TestCase', true)) { + class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); +} + +/** + * Custom logger class, prints to screen + */ +class testConsoleLogger implements LoggerInterface { + public function log($message, $context = array()) { + echo $message .PHP_EOL; + } +} + +class csrfp_test_customLogger extends PHPUnit_Framework_TestCase +{ + /** + * @var array to hold current configurations + */ + protected $config = array(); + + public function setUp() { + csrfprotector::$config['CSRFP_TOKEN'] = 'csrfp_token'; + csrfprotector::$config['cookieConfig'] = array('secure' => false); + csrfprotector::$config['logDirectory'] = '../test/logs'; + $_SERVER['REQUEST_URI'] = 'temp'; // For logging + $_SERVER['REQUEST_SCHEME'] = 'http'; // For authorizePost + $_SERVER['HTTP_HOST'] = 'test'; // For isUrlAllowed + $_SERVER['PHP_SELF'] = '/index.php'; // For authorizePost + $_POST[csrfprotector::$config['CSRFP_TOKEN']] + = $_GET[csrfprotector::$config['CSRFP_TOKEN']] = '123'; + + //token mismatch - leading to failed validation + $_SESSION[csrfprotector::$config['CSRFP_TOKEN']] = array('abc'); + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; + $_SERVER['HTTPS'] = null; + + $this->config = include(__DIR__ .'/config.test.php'); + + // Create an instance of config file -- for testing + $data = file_get_contents(__DIR__ .'/config.test.php'); + file_put_contents(__DIR__ .'/../libs/config.php', $data); + + if (!defined('__CSRFP_UNIT_TEST__')) define('__CSRFP_UNIT_TEST__', true); + } + + /** + * tearDown() + */ + public function tearDown() + { + unlink(__DIR__ .'/../libs/config.php'); + if (is_dir(__DIR__ .'/logs')) + Helper::delTree(__DIR__ .'/logs'); + } + + /** + * To test a custom logger class + */ + public function testCustomLogger() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $tmp = csrfProtector::$config; + csrfProtector::$config = array(); + csrfProtector::init(null, array('POST' => 1), new testConsoleLogger()); + + $this->assertTrue(true); + csrfProtector::$config = $tmp; + } +} \ No newline at end of file