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