CsrfProtectionMiddleware.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.5.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Http\Middleware;
  16. use Cake\Http\Cookie\Cookie;
  17. use Cake\Http\Exception\InvalidCsrfTokenException;
  18. use Cake\Http\Response;
  19. use Cake\Http\ServerRequest;
  20. use Cake\I18n\Time;
  21. use Cake\Utility\Hash;
  22. use Cake\Utility\Security;
  23. /**
  24. * Provides CSRF protection & validation.
  25. *
  26. * This middleware adds a CSRF token to a cookie. The cookie value is compared to
  27. * request data, or the X-CSRF-Token header on each PATCH, POST,
  28. * PUT, or DELETE request.
  29. *
  30. * If the request data is missing or does not match the cookie data,
  31. * an InvalidCsrfTokenException will be raised.
  32. *
  33. * This middleware integrates with the FormHelper automatically and when
  34. * used together your forms will have CSRF tokens automatically added
  35. * when `$this->Form->create(...)` is used in a view.
  36. */
  37. class CsrfProtectionMiddleware
  38. {
  39. /**
  40. * Default config for the CSRF handling.
  41. *
  42. * - `cookieName` The name of the cookie to send.
  43. * - `expiry` A strotime compatible value of how long the CSRF token should last.
  44. * Defaults to browser session.
  45. * - `secure` Whether or not the cookie will be set with the Secure flag. Defaults to false.
  46. * - `httpOnly` Whether or not the cookie will be set with the HttpOnly flag. Defaults to false.
  47. * - `samesite` Value for "SameSite" attribute. Default to null.
  48. * - `field` The form field to check. Changing this will also require configuring
  49. * FormHelper.
  50. *
  51. * @var array
  52. */
  53. protected $_defaultConfig = [
  54. 'cookieName' => 'csrfToken',
  55. 'expiry' => 0,
  56. 'secure' => false,
  57. 'httpOnly' => false,
  58. 'samesite' => null,
  59. 'field' => '_csrfToken',
  60. ];
  61. /**
  62. * Configuration
  63. *
  64. * @var array
  65. */
  66. protected $_config = [];
  67. /**
  68. * Callback for deciding whether or not to skip the token check for particular request.
  69. *
  70. * CSRF protection token check will be skipped if the callback returns `true`.
  71. *
  72. * @var callable|null
  73. */
  74. protected $whitelistCallback;
  75. /**
  76. * Constructor
  77. *
  78. * @param array $config Config options. See $_defaultConfig for valid keys.
  79. */
  80. public function __construct(array $config = [])
  81. {
  82. $this->_config = $config + $this->_defaultConfig;
  83. }
  84. /**
  85. * Checks and sets the CSRF token depending on the HTTP verb.
  86. *
  87. * @param \Cake\Http\ServerRequest $request The request.
  88. * @param \Cake\Http\Response $response The response.
  89. * @param callable $next Callback to invoke the next middleware.
  90. * @return \Cake\Http\Response A response
  91. */
  92. public function __invoke(ServerRequest $request, Response $response, $next)
  93. {
  94. if (
  95. $this->whitelistCallback !== null
  96. && call_user_func($this->whitelistCallback, $request) === true
  97. ) {
  98. return $next($request, $response);
  99. }
  100. $cookies = $request->getCookieParams();
  101. $cookieData = Hash::get($cookies, $this->_config['cookieName']);
  102. if (is_string($cookieData) && strlen($cookieData) > 0) {
  103. $params = $request->getAttribute('params');
  104. $params['_csrfToken'] = $cookieData;
  105. $request = $request->withAttribute('params', $params);
  106. }
  107. $method = $request->getMethod();
  108. if ($method === 'GET' && $cookieData === null) {
  109. $token = $this->_createToken();
  110. $request = $this->_addTokenToRequest($token, $request);
  111. $response = $this->_addTokenCookie($token, $request, $response);
  112. return $next($request, $response);
  113. }
  114. $request = $this->_validateAndUnsetTokenField($request);
  115. return $next($request, $response);
  116. }
  117. /**
  118. * Set callback for allowing to skip token check for particular request.
  119. *
  120. * The callback will receive request instance as argument and must return
  121. * `true` if you want to skip token check for the current request.
  122. *
  123. * @param callable $callback A callable.
  124. * @return $this
  125. */
  126. public function whitelistCallback(callable $callback)
  127. {
  128. $this->whitelistCallback = $callback;
  129. return $this;
  130. }
  131. /**
  132. * Checks if the request is POST, PUT, DELETE or PATCH and validates the CSRF token
  133. *
  134. * @param \Cake\Http\ServerRequest $request The request object.
  135. * @return \Cake\Http\ServerRequest
  136. */
  137. protected function _validateAndUnsetTokenField(ServerRequest $request)
  138. {
  139. if (in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true) || $request->getData()) {
  140. $this->_validateToken($request);
  141. $body = $request->getParsedBody();
  142. if (is_array($body)) {
  143. unset($body[$this->_config['field']]);
  144. $request = $request->withParsedBody($body);
  145. }
  146. }
  147. return $request;
  148. }
  149. /**
  150. * Create a new token to be used for CSRF protection
  151. *
  152. * @return string
  153. */
  154. protected function _createToken()
  155. {
  156. return hash('sha512', Security::randomBytes(16), false);
  157. }
  158. /**
  159. * Add a CSRF token to the request parameters.
  160. *
  161. * @param string $token The token to add.
  162. * @param \Cake\Http\ServerRequest $request The request to augment
  163. * @return \Cake\Http\ServerRequest Modified request
  164. */
  165. protected function _addTokenToRequest($token, ServerRequest $request)
  166. {
  167. $params = $request->getAttribute('params');
  168. $params['_csrfToken'] = $token;
  169. return $request->withAttribute('params', $params);
  170. }
  171. /**
  172. * Add a CSRF token to the response cookies.
  173. *
  174. * @param string $token The token to add.
  175. * @param \Cake\Http\ServerRequest $request The request to validate against.
  176. * @param \Cake\Http\Response $response The response.
  177. * @return \Cake\Http\Response $response Modified response.
  178. */
  179. protected function _addTokenCookie($token, ServerRequest $request, Response $response)
  180. {
  181. $expiry = new Time($this->_config['expiry']);
  182. $cookie = new Cookie(
  183. $this->_config['cookieName'],
  184. $token,
  185. $expiry,
  186. $request->getAttribute('webroot'),
  187. '',
  188. (bool)$this->_config['secure'],
  189. (bool)$this->_config['httpOnly'],
  190. isset($this->_config['samesite']) ? $this->_config['samesite'] : $this->_defaultConfig['samesite']
  191. );
  192. return $response->withCookie($cookie);
  193. }
  194. /**
  195. * Validate the request data against the cookie token.
  196. *
  197. * @param \Cake\Http\ServerRequest $request The request to validate against.
  198. * @return void
  199. * @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing.
  200. */
  201. protected function _validateToken(ServerRequest $request)
  202. {
  203. $cookies = $request->getCookieParams();
  204. $cookie = Hash::get($cookies, $this->_config['cookieName']);
  205. $post = Hash::get($request->getParsedBody(), $this->_config['field']);
  206. $header = $request->getHeaderLine('X-CSRF-Token');
  207. if (!$cookie) {
  208. throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
  209. }
  210. if (!Security::constantEquals($post, $cookie) && !Security::constantEquals($header, $cookie)) {
  211. throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
  212. }
  213. }
  214. }