CsrfProtectionMiddleware.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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\I18n\Time;
  17. use Cake\Network\Exception\InvalidCsrfTokenException;
  18. use Cake\Utility\Security;
  19. use Psr\Http\Message\ResponseInterface;
  20. use Psr\Http\Message\ServerRequestInterface;
  21. /**
  22. * Provides CSRF protection & validation.
  23. *
  24. * This component adds a CSRF token to a cookie. The cookie value is compared to
  25. * request data, or the X-CSRF-Token header on each PATCH, POST,
  26. * PUT, or DELETE request.
  27. *
  28. * If the request data is missing or does not match the cookie data,
  29. * an InvalidCsrfTokenException will be raised.
  30. *
  31. * This middleware integrates with the FormHelper automatically and when
  32. * used together your forms will have CSRF tokens automatically added
  33. * when `$this->Form->create(...)` is used in a view.
  34. */
  35. class CsrfProtectionMiddleware
  36. {
  37. /**
  38. * Default config for the CSRF handling.
  39. *
  40. * - cookieName = The name of the cookie to send.
  41. * - expiry = How long the CSRF token should last. Defaults to browser session.
  42. * - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false.
  43. * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false.
  44. * - field = The form field to check. Changing this will also require configuring
  45. * FormHelper.
  46. *
  47. * @var array
  48. */
  49. protected $_defaultConfig = [
  50. 'cookieName' => 'csrfToken',
  51. 'expiry' => 0,
  52. 'secure' => false,
  53. 'httpOnly' => false,
  54. 'field' => '_csrfToken',
  55. ];
  56. /**
  57. * Configuration
  58. *
  59. * @var array
  60. */
  61. protected $_config = [];
  62. /**
  63. * Constructor
  64. *
  65. * @param array $config Config options
  66. */
  67. public function __construct(array $config = [])
  68. {
  69. $this->_config = $config + $this->_defaultConfig;
  70. }
  71. /**
  72. * Serve assets if the path matches one.
  73. *
  74. * @param \Psr\Http\Message\ServerRequestInterface $request The request.
  75. * @param \Psr\Http\Message\ResponseInterface $response The response.
  76. * @param callable $next Callback to invoke the next middleware.
  77. * @return \Psr\Http\Message\ResponseInterface A response
  78. */
  79. public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
  80. {
  81. $cookies = $request->getCookieParams();
  82. $cookieData = null;
  83. if (isset($cookies[$this->_config['cookieName']])) {
  84. $cookieData = $cookies[$this->_config['cookieName']];
  85. }
  86. if (!empty($cookieData)) {
  87. $params = $request->getAttribute('params');
  88. $params['_csrfToken'] = $cookieData;
  89. $request = $request->withAttribute('params', $params);
  90. }
  91. $requested = $request->getParam('requested');
  92. if ($requested === 1) {
  93. return $next($request, $response);
  94. }
  95. $method = $request->getMethod();
  96. if ($method === 'GET' && $cookieData === null) {
  97. $this->_setToken($request, $response);
  98. return $next($request, $response);
  99. }
  100. $request = $this->_validateAndUnsetTokenField($request);
  101. return $next($request, $response);
  102. }
  103. /**
  104. * Checks if the request is POST, PUT, DELETE or PATCH and validates the CSRF token
  105. *
  106. * @param \Cake\Http\ServerRequest $request The request object.
  107. * @return void
  108. */
  109. protected function _validateAndUnsetTokenField(ServerRequestInterface $request)
  110. {
  111. if (in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH']) || $request->getData()) {
  112. $this->_validateToken($request);
  113. $body = $request->getParsedBody();
  114. if (is_array($body)) {
  115. unset($body[$this->_config['field']]);
  116. $request = $request->withParsedBody($body);
  117. }
  118. }
  119. return $request;
  120. }
  121. /**
  122. * Set the token in the response.
  123. *
  124. * Also sets the request->params['_csrfToken'] so the newly minted
  125. * token is available in the request data.
  126. *
  127. * @param \Cake\Http\ServerRequest $request The request object.
  128. * @param \Cake\Http\Response $response The response object.
  129. * @return void
  130. */
  131. protected function _setToken(ServerRequestInterface &$request, ResponseInterface &$response)
  132. {
  133. $expiry = new Time($this->_config['expiry']);
  134. $value = hash('sha512', Security::randomBytes(16), false);
  135. $params = $request->getAttribute('params');
  136. $params['_csrfToken'] = $value;
  137. $request = $request->withAttribute('params', $params);
  138. $response = $response->withCookie($this->_config['cookieName'], [
  139. 'value' => $value,
  140. 'expire' => $expiry->format('U'),
  141. 'path' => $request->getAttribute('webroot'),
  142. 'secure' => $this->_config['secure'],
  143. 'httpOnly' => $this->_config['httpOnly'],
  144. ]);
  145. }
  146. /**
  147. * Validate the request data against the cookie token.
  148. *
  149. * @param \Cake\Http\ServerRequest $request The request to validate against.
  150. * @throws \Cake\Network\Exception\InvalidCsrfTokenException when the CSRF token is invalid or missing.
  151. * @return void
  152. */
  153. protected function _validateToken(ServerRequestInterface $request)
  154. {
  155. $cookies = $request->getCookieParams();
  156. $cookie = isset($cookies[$this->_config['cookieName']]) ? $cookies[$this->_config['cookieName']] : null;
  157. $post = $request->getData($this->_config['field']);
  158. $header = $request->getHeaderLine('X-CSRF-Token');
  159. if (!$cookie) {
  160. throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
  161. }
  162. if ($post !== $cookie && $header !== $cookie) {
  163. throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
  164. }
  165. }
  166. }