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: * Callback for deciding whether or not to skip the token check for particular request.
71: *
72: * CSRF protection token check will be skipped if the callback returns `true`.
73: *
74: * @var callable|null
75: */
76: protected $whitelistCallback;
77:
78: /**
79: * Constructor
80: *
81: * @param array $config Config options. See $_defaultConfig for valid keys.
82: */
83: public function __construct(array $config = [])
84: {
85: $this->_config = $config + $this->_defaultConfig;
86: }
87:
88: /**
89: * Checks and sets the CSRF token depending on the HTTP verb.
90: *
91: * @param \Cake\Http\ServerRequest $request The request.
92: * @param \Cake\Http\Response $response The response.
93: * @param callable $next Callback to invoke the next middleware.
94: * @return \Cake\Http\Response A response
95: */
96: public function __invoke(ServerRequest $request, Response $response, $next)
97: {
98: if ($this->whitelistCallback !== null
99: && call_user_func($this->whitelistCallback, $request) === true
100: ) {
101: return $next($request, $response);
102: }
103:
104: $cookies = $request->getCookieParams();
105: $cookieData = Hash::get($cookies, $this->_config['cookieName']);
106:
107: if (strlen($cookieData) > 0) {
108: $params = $request->getAttribute('params');
109: $params['_csrfToken'] = $cookieData;
110: $request = $request->withAttribute('params', $params);
111: }
112:
113: $method = $request->getMethod();
114: if ($method === 'GET' && $cookieData === null) {
115: $token = $this->_createToken();
116: $request = $this->_addTokenToRequest($token, $request);
117: $response = $this->_addTokenCookie($token, $request, $response);
118:
119: return $next($request, $response);
120: }
121: $request = $this->_validateAndUnsetTokenField($request);
122:
123: return $next($request, $response);
124: }
125:
126: /**
127: * Set callback for allowing to skip token check for particular request.
128: *
129: * The callback will receive request instance as argument and must return
130: * `true` if you want to skip token check for the current request.
131: *
132: * @param callable $callback A callable.
133: * @return $this
134: */
135: public function whitelistCallback(callable $callback)
136: {
137: $this->whitelistCallback = $callback;
138:
139: return $this;
140: }
141:
142: /**
143: * Checks if the request is POST, PUT, DELETE or PATCH and validates the CSRF token
144: *
145: * @param \Cake\Http\ServerRequest $request The request object.
146: * @return \Cake\Http\ServerRequest
147: */
148: protected function _validateAndUnsetTokenField(ServerRequest $request)
149: {
150: if (in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true) || $request->getData()) {
151: $this->_validateToken($request);
152: $body = $request->getParsedBody();
153: if (is_array($body)) {
154: unset($body[$this->_config['field']]);
155: $request = $request->withParsedBody($body);
156: }
157: }
158:
159: return $request;
160: }
161:
162: /**
163: * Create a new token to be used for CSRF protection
164: *
165: * @return string
166: */
167: protected function _createToken()
168: {
169: return hash('sha512', Security::randomBytes(16), false);
170: }
171:
172: /**
173: * Add a CSRF token to the request parameters.
174: *
175: * @param string $token The token to add.
176: * @param \Cake\Http\ServerRequest $request The request to augment
177: * @return \Cake\Http\ServerRequest Modified request
178: */
179: protected function _addTokenToRequest($token, ServerRequest $request)
180: {
181: $params = $request->getAttribute('params');
182: $params['_csrfToken'] = $token;
183:
184: return $request->withAttribute('params', $params);
185: }
186:
187: /**
188: * Add a CSRF token to the response cookies.
189: *
190: * @param string $token The token to add.
191: * @param \Cake\Http\ServerRequest $request The request to validate against.
192: * @param \Cake\Http\Response $response The response.
193: * @return \Cake\Http\Response $response Modified response.
194: */
195: protected function _addTokenCookie($token, ServerRequest $request, Response $response)
196: {
197: $expiry = new Time($this->_config['expiry']);
198:
199: $cookie = new Cookie(
200: $this->_config['cookieName'],
201: $token,
202: $expiry,
203: $request->getAttribute('webroot'),
204: '',
205: (bool)$this->_config['secure'],
206: (bool)$this->_config['httpOnly']
207: );
208:
209: return $response->withCookie($cookie);
210: }
211:
212: /**
213: * Validate the request data against the cookie token.
214: *
215: * @param \Cake\Http\ServerRequest $request The request to validate against.
216: * @return void
217: * @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing.
218: */
219: protected function _validateToken(ServerRequest $request)
220: {
221: $cookies = $request->getCookieParams();
222: $cookie = Hash::get($cookies, $this->_config['cookieName']);
223: $post = Hash::get($request->getParsedBody(), $this->_config['field']);
224: $header = $request->getHeaderLine('X-CSRF-Token');
225:
226: if (!$cookie) {
227: throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
228: }
229:
230: if (!Security::constantEquals($post, $cookie) && !Security::constantEquals($header, $cookie)) {
231: throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
232: }
233: }
234: }
235: