/classes/csrf/security.php

https://github.com/gevans/kohana-protect-from-forgery · PHP · 197 lines · 77 code · 23 blank · 97 comment · 9 complexity · da54833221a6b3fde2d490a54f931946 MD5 · raw file

  1. <?php defined('SYSPATH') or die('No direct script access.');
  2. /**
  3. * Security helper extension for supporting multiple CSRF tokens with
  4. * expirations, improved hashing, easier configuration, and drop-in
  5. * support for validation.
  6. *
  7. * @package Protect From Forgery
  8. * @category Security
  9. * @author Gabriel Evans <gabriel@codeconcoction.com>
  10. * @copyright (c) 2011 Gabriel Evans
  11. * @license http://www.opensource.org/licenses/mit-license.php
  12. */
  13. class CSRF_Security extends Kohana_Security
  14. {
  15. /**
  16. * @var string current request token
  17. */
  18. public static $token = NULL;
  19. /**
  20. * @var integer time in seconds until a token expires
  21. */
  22. public static $token_lifetime = 900;
  23. /**
  24. * @var integer max number of tokens stored in a single session
  25. */
  26. public static $token_limit = 50;
  27. /**
  28. * @var string key used for hashing tokens
  29. */
  30. public static $token_secret = NULL;
  31. /**
  32. * @var string string appended to tokens prior to hashing
  33. */
  34. public static $token_salt = NULL;
  35. /**
  36. * @var string token key name used for session storage, forms, and headers
  37. */
  38. public static $token_name = 'authenticity_token';
  39. /**
  40. * Check that the given token matches the currently stored security token.
  41. *
  42. * if (Security::check($token))
  43. * {
  44. * // Pass
  45. * }
  46. *
  47. * @param string token to check
  48. * @return boolean
  49. * @uses Session::instance
  50. */
  51. public static function check($token)
  52. {
  53. $session = Session::instance();
  54. // Retrieve tokens from session
  55. $tokens = $session->get(Security::$token_name, array());
  56. if (isset($tokens[$token]) AND $tokens[$token] > time())
  57. {
  58. // Token found, remove it from the array
  59. unset($tokens[$token]);
  60. // Store the updated tokens array
  61. $session->set(Security::$token_name, $tokens);
  62. return TRUE;
  63. }
  64. return FALSE;
  65. }
  66. /**
  67. * Generate and store a unique token which can be used to help prevent
  68. * [CSRF](http://wikipedia.org/wiki/Cross_Site_Request_Forgery) attacks.
  69. *
  70. * $token = Security::token();
  71. *
  72. * You can insert this token into your forms as a hidden field:
  73. *
  74. * echo Form::csrf_param();
  75. *
  76. * And then check it when using [Validation]:
  77. *
  78. * $array->rules(Security::$token_name, array(
  79. * 'not_empty' => NULL,
  80. * 'Security::check' => NULL,
  81. * ));
  82. *
  83. * Or check it in a `before()` filter in your controllers:
  84. *
  85. * public function before()
  86. * {
  87. * $this->protect_from_forgery();
  88. * }
  89. *
  90. * This provides a basic, but effective, method of preventing CSRF attacks.
  91. *
  92. * @param boolean force a new token to be generated?
  93. * @return string
  94. * @uses Session::instance
  95. * @see Controller::protect_from_forgery
  96. */
  97. public static function token($new = FALSE)
  98. {
  99. if (Security::$token === NULL)
  100. {
  101. $session = Session::instance();
  102. // Get the current tokens
  103. $tokens = $session->get(Security::$token_name);
  104. if (count($tokens) > Security::$token_limit)
  105. {
  106. // Remove oldest token from the array
  107. array_shift($tokens);
  108. }
  109. // Generate a new unique token
  110. Security::$token = $token = base64_encode(hash_hmac('sha256', uniqid(NULL, TRUE).Security::$token_salt, Security::$token_secret, TRUE));
  111. // Add to tokens and give an expiration
  112. $tokens[$token] = time() + Security::$token_lifetime;
  113. // Store the updated tokens array
  114. $session->set(Security::$token_name, $tokens);
  115. }
  116. return Security::$token;
  117. }
  118. /**
  119. * Checks external non-GET requests for a valid CSRF token. If a token is
  120. * missing or invalid, it will attempt to redirect to the referrer,
  121. * most likely a form, or throw an exception when the referrer is `NULL`.
  122. *
  123. * @param Request $request Request instance
  124. * @param mixed $callback Callback for validation success
  125. * @return void
  126. * @throws CSRF_Validation_Exception
  127. */
  128. public static function protect_from_forgery(Request $request, $callback = NULL)
  129. {
  130. if ( ! $request->is_external() OR $request->method() === Request::GET)
  131. {
  132. // Skip validation for internal and GET requests
  133. return;
  134. }
  135. if ($request->post(Security::$token_name) !== NULL)
  136. {
  137. // Set the token from POST parameters
  138. $token = $request->post(Security::$token_name);
  139. }
  140. else
  141. {
  142. // If the CSRF token is missing in the POST params, fallback to header
  143. $token = $request->headers('x-csrf-token');
  144. }
  145. if (Security::check($token))
  146. {
  147. if ($callback !== NULL)
  148. {
  149. // Send success to callback
  150. call_user_func($callback, TRUE);
  151. }
  152. }
  153. else
  154. {
  155. if ($callback !== NULL)
  156. {
  157. // Send failure to callback
  158. call_user_func($callback, FALSE);
  159. }
  160. if ($request->referrer() !== NULL)
  161. {
  162. // If the client has a referrer, redirect to it
  163. $request->redirect($request->referrer());
  164. }
  165. else
  166. {
  167. // If the client has no referrer, throw an informative exception
  168. throw new CSRF_Validation_Exception('Expected valid CSRF token parameter, :token_name, or X-CSRF-Token header', array(
  169. ':token_name' => Security::$token_name,
  170. ));
  171. }
  172. }
  173. }
  174. } // End Security