Form.php (mrbs-1.9.4) | : | Form.php (mrbs-1.10.0) | ||
---|---|---|---|---|
<?php | <?php | |||
namespace MRBS\Form; | namespace MRBS\Form; | |||
use MRBS\Exception; | ||||
use MRBS\JFactory; | use MRBS\JFactory; | |||
use MRBS\Session\SessionCookie; | use MRBS\Session\SessionCookie; | |||
use function MRBS\fatal_error; | ||||
use function MRBS\generate_token; | ||||
use function MRBS\get_form_var; | ||||
use function MRBS\get_vocab; | ||||
use function MRBS\session; | ||||
class Form extends Element | class Form extends Element | |||
{ | { | |||
private static $token = null; | private static $token = null; | |||
private static $token_name = 'csrf_token'; // As of PHP 7.1 this would be a p rivate const | private static $token_name = 'csrf_token'; // As of PHP 7.1 this would be a p rivate const | |||
private static $cookie_set = false; | private static $cookie_set = false; | |||
public function __construct() | public function __construct() | |||
{ | { | |||
parent::__construct('form'); | parent::__construct('form'); | |||
$this->addCSRFToken(); | $this->addCSRFToken(); | |||
} | } | |||
// Adds a hidden input to the form | // Adds a hidden input to the form | |||
public function addHiddenInput($name, $value) | public function addHiddenInput($name, $value) : Form | |||
{ | { | |||
$element = new ElementInputHidden(); | $element = new ElementInputHidden(); | |||
$element->setAttributes(array('name' => $name, | $element->setAttributes(array('name' => $name, | |||
'value' => $value)); | 'value' => $value)); | |||
$this->addElement($element); | $this->addElement($element); | |||
return $this; | return $this; | |||
} | } | |||
// Adds an array of hidden inputs to the form | // Adds an array of hidden inputs to the form | |||
public function addHiddenInputs(array $hidden_inputs) | public function addHiddenInputs(array $hidden_inputs) : Form | |||
{ | { | |||
foreach ($hidden_inputs as $key => $value) | foreach ($hidden_inputs as $key => $value) | |||
{ | { | |||
$this->addHiddenInput($key, $value); | $this->addHiddenInput($key, $value); | |||
} | } | |||
return $this; | return $this; | |||
} | } | |||
// Returns the HTML for a hidden field containing a CSRF token | // Returns the HTML for a hidden field containing a CSRF token | |||
public static function getTokenHTML() | public static function getTokenHTML() : string | |||
{ | { | |||
$element = new ElementInputHidden(); | $element = new ElementInputHidden(); | |||
$element->setAttributes(array('name' => self::$token_name, | $element->setAttributes(array('name' => self::$token_name, | |||
'value' => self::getToken())); | 'value' => self::getToken())); | |||
return $element->toHTML(); | return $element->toHTML(); | |||
} | } | |||
// Checks the CSRF token against the stored value and dies with a fatal error | // Checks the CSRF token against the stored value and dies with a fatal error | |||
// if they do not match. Note that: | // if they do not match. Note that: | |||
// (1) The CSRF token is always looked for in the POST data, never anywhere else. | // (1) The CSRF token is always looked for in the POST data, never anywhere else. | |||
skipping to change at line 63 | skipping to change at line 69 | |||
// grant access. | // grant access. | |||
// (2) Forms should normally use a POST method. | // (2) Forms should normally use a POST method. | |||
// (3) Actions should normally be taken by handler pages which are not desi gned to be | // (3) Actions should normally be taken by handler pages which are not desi gned to be | |||
// accessed directly by the user and are only expecting POST requests. These pages | // accessed directly by the user and are only expecting POST requests. These pages | |||
// will look for the CSRF token however they are requested. If they ar e requested via | // will look for the CSRF token however they are requested. If they ar e requested via | |||
// GET then they will still look for the token in the POST data and so fail. | // GET then they will still look for the token in the POST data and so fail. | |||
// (4) There are some MRBS pages that can be accessed either via a URL with query string, | // (4) There are some MRBS pages that can be accessed either via a URL with query string, | |||
// or via a POST request. These pages should not take any action, but as a matter of | // or via a POST request. These pages should not take any action, but as a matter of | |||
// good practice should check the token anyway if they have been reques ted by a POST. | // good practice should check the token anyway if they have been reques ted by a POST. | |||
// To cater for these pages the $post_only parameter should be set to T RUE. | // To cater for these pages the $post_only parameter should be set to T RUE. | |||
public static function checkToken($post_only=false) | public static function checkToken($post_only=false) : void | |||
{ | { | |||
global $server; | global $server; | |||
if ($post_only && ($server['REQUEST_METHOD'] != 'POST')) | if ($post_only && ($server['REQUEST_METHOD'] != 'POST')) | |||
{ | { | |||
return; | return; | |||
} | } | |||
$token = \MRBS\get_form_var(self::$token_name, 'string', null, INPUT_POST); | $token = get_form_var(self::$token_name, 'string', null, INPUT_POST); | |||
$stored_token = self::getStoredToken(); | $stored_token = self::getStoredToken(); | |||
if (!self::compareTokens($stored_token, $token)) | if (!self::compareTokens($stored_token, $token)) | |||
{ | { | |||
if (isset($stored_token)) | if (isset($stored_token)) | |||
{ | { | |||
// Only report a possible CSRF attack if the stored token exists. If i t doesn't | // Only report a possible CSRF attack if the stored token exists. If i t doesn't | |||
// it's normally because the user session has expired in between the for m being | // it's normally because the user session has expired in between the for m being | |||
// displayed and submitted. | // displayed and submitted. | |||
trigger_error('Possible CSRF attack from IP address ' . $server['REMOTE_ ADDR'], E_USER_NOTICE); | trigger_error('Possible CSRF attack from IP address ' . $server['REMOTE_ ADDR'], E_USER_NOTICE); | |||
} | } | |||
if (method_exists(\MRBS\session(), 'logoffUser')) | if (method_exists(session(), 'logoffUser')) | |||
{ | { | |||
\MRBS\session()->logoffUser(); | session()->logoffUser(); | |||
} | } | |||
\MRBS\fatal_error(\MRBS\get_vocab("session_expired")); | fatal_error(get_vocab("session_expired")); | |||
} | } | |||
} | } | |||
// $max_unit can be set to 'seconds', 'minutes', 'hours', etc. and | // $max_unit can be set to 'seconds', 'minutes', 'hours', etc. and | |||
// can be used to specify the maximum unit to return. | // can be used to specify the maximum unit to return. | |||
public static function getTimeUnitOptions($max_unit=null) | public static function getTimeUnitOptions($max_unit=null) : array | |||
{ | { | |||
$options = array(); | $options = array(); | |||
$units = array('seconds', 'minutes', 'hours', 'days', 'weeks'); | $units = array('seconds', 'minutes', 'hours', 'days', 'weeks'); | |||
foreach ($units as $unit) | foreach ($units as $unit) | |||
{ | { | |||
$options[$unit] = \MRBS\get_vocab($unit); | $options[$unit] = get_vocab($unit); | |||
if (isset($max_unit) && ($max_unit == $unit)) | if (isset($max_unit) && ($max_unit == $unit)) | |||
{ | { | |||
break; | break; | |||
} | } | |||
} | } | |||
return $options; | return $options; | |||
} | } | |||
private function addCSRFToken() | private function addCSRFToken() : Form | |||
{ | { | |||
$this->addHiddenInput(self::$token_name, self::getToken()); | $this->addHiddenInput(self::$token_name, self::getToken()); | |||
return $this; | return $this; | |||
} | } | |||
// Get a CSRF token | // Get a CSRF token | |||
public static function getToken() | public static function getToken() : string | |||
{ | { | |||
$token_length = 32; | $token_length = 32; | |||
if (!isset(self::$token)) | if (!isset(self::$token)) | |||
{ | { | |||
$stored_token = self::getStoredToken(); | $stored_token = self::getStoredToken(); | |||
if (isset($stored_token)) | // The test below should really be isset() rather than !empty(). However | |||
occasionally MRBS has the | ||||
// value 0 stored in the session variable. It's not clear how or why this | ||||
is happening. Until the | ||||
// root cause is found we test for empty() and if the token is set but emp | ||||
ty we generate a new token. | ||||
if (!empty($stored_token)) | ||||
{ | { | |||
self::$token = $stored_token; | self::$token = $stored_token; | |||
} | } | |||
else | else | |||
{ | { | |||
self::$token = \MRBS\generate_token($token_length); | if (isset($stored_token)) | |||
{ | ||||
// The token is set but empty | ||||
$message = "Stored token is '$stored_token'. This should not be possi | ||||
ble. " . | ||||
"Generating a new token."; | ||||
trigger_error($message,E_USER_WARNING); | ||||
} | ||||
self::$token = generate_token($token_length); | ||||
self::storeToken(self::$token); | self::storeToken(self::$token); | |||
} | } | |||
} | } | |||
return self::$token; | return self::$token; | |||
} | } | |||
// Compare two tokens in a timing attack safe manner. | // Compare two tokens in a timing attack safe manner. | |||
// Returns true if they are equal, otherwise false. | // Returns true if they are equal, otherwise false. | |||
// Note: it is important to provide the user-supplied string as the | // Note: it is important to provide the user-supplied string as the | |||
// second parameter, rather than the first. | // second parameter, rather than the first. | |||
private static function compareTokens($known_token, $user_token) | private static function compareTokens($known_token, $user_token) : bool | |||
{ | { | |||
if (is_null($known_token) || is_null($user_token)) | if (is_null($known_token) || is_null($user_token)) | |||
{ | { | |||
return false; | return false; | |||
} | } | |||
if (function_exists('hash_equals')) | if (function_exists('hash_equals')) | |||
{ | { | |||
return hash_equals($known_token, $user_token); | return hash_equals($known_token, $user_token); | |||
} | } | |||
// Could do fancier things here to give a timing attack safe comparison, | // Could do fancier things here to give a timing attack safe comparison, | |||
// For example https://github.com/indigophp/hash-compat | // For example https://github.com/indigophp/hash-compat | |||
return ($known_token === $user_token); | return ($known_token === $user_token); | |||
} | } | |||
private static function storeToken($token) | private static function storeToken($token) : void | |||
{ | { | |||
global $auth, $csrf_cookie; | global $auth, $csrf_cookie; | |||
if ($auth['session'] == 'joomla') | if ($auth['session'] == 'joomla') | |||
{ | { | |||
// Joomla has its own session handling and will clear the $_SESSION variab le, | // Joomla has its own session handling and will clear the $_SESSION variab le, | |||
// so if we are using Joomla authentication we need to do sessions the Joo mla | // so if we are using Joomla authentication we need to do sessions the Joo mla | |||
// way. (Maybe MRBS should abstract session handling into a separate Ses sion | // way. (Maybe MRBS should abstract session handling into a separate Ses sion | |||
// class in due course? Note also that Joomla's JSession class has metho ds for | // class in due course? Note also that Joomla's JSession class has metho ds for | |||
// getting and checking form tokens, so maybe that's another way of doing it?) | // getting and checking form tokens, so maybe that's another way of doing it?) | |||
skipping to change at line 186 | skipping to change at line 202 | |||
$session_status = session_status(); | $session_status = session_status(); | |||
// Use PHP sessions if we can | // Use PHP sessions if we can | |||
if ($session_status !== PHP_SESSION_DISABLED) | if ($session_status !== PHP_SESSION_DISABLED) | |||
{ | { | |||
if ($session_status === PHP_SESSION_NONE) | if ($session_status === PHP_SESSION_NONE) | |||
{ | { | |||
if (false === session_start()) | if (false === session_start()) | |||
{ | { | |||
throw new \Exception("Could not start session"); | throw new Exception("Could not start session"); | |||
} | } | |||
} | } | |||
$_SESSION[self::$token_name] = $token; | $_SESSION[self::$token_name] = $token; | |||
return; | return; | |||
} | } | |||
// Otherwise use cookies | // Otherwise use cookies | |||
if (!self::$cookie_set) | if (!self::$cookie_set) | |||
{ | { | |||
SessionCookie::setCookie('MRBS_CSRF', | SessionCookie::setCookie('MRBS_CSRF', | |||
$csrf_cookie['hash_algorithm'], | $csrf_cookie['hash_algorithm'], | |||
$csrf_cookie['secret'], | $csrf_cookie['secret'], | |||
array(self::$token_name => $token), | array(self::$token_name => $token), | |||
0); //Always a session cookie | 0); //Always a session cookie | |||
self::$cookie_set = true; | self::$cookie_set = true; | |||
} | } | |||
} | } | |||
private static function getStoredToken() | private static function getStoredToken() : ?string | |||
{ | { | |||
global $auth, $csrf_cookie; | global $auth, $csrf_cookie; | |||
if ($auth['session'] == 'joomla') | if ($auth['session'] == 'joomla') | |||
{ | { | |||
$session = JFactory::getSession(); | $session = JFactory::getSession(); | |||
return $session->get(self::$token_name); | return $session->get(self::$token_name); | |||
} | } | |||
$session_status = session_status(); | $session_status = session_status(); | |||
// Use PHP sessions if we can | // Use PHP sessions if we can | |||
if ($session_status !== PHP_SESSION_DISABLED) | if ($session_status !== PHP_SESSION_DISABLED) | |||
{ | { | |||
if ($session_status === PHP_SESSION_NONE) | if ($session_status === PHP_SESSION_NONE) | |||
{ | { | |||
if (false === session_start()) | if (false === session_start()) | |||
{ | { | |||
throw new \Exception("Could not start session"); | throw new Exception("Could not start session"); | |||
} | } | |||
} | } | |||
return (isset($_SESSION[self::$token_name])) ? $_SESSION[self::$token_name ] : null; | return (isset($_SESSION[self::$token_name])) ? $_SESSION[self::$token_name ] : null; | |||
} | } | |||
// Otherwise use cookies | // Otherwise use cookies | |||
$data = SessionCookie::getCookie('MRBS_CSRF', | $data = SessionCookie::getCookie('MRBS_CSRF', | |||
$csrf_cookie['hash_algorithm'], | $csrf_cookie['hash_algorithm'], | |||
$csrf_cookie['secret']); | $csrf_cookie['secret']); | |||
End of changes. 21 change blocks. | ||||
19 lines changed or deleted | 39 lines changed or added |