1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Controller\Component;
16:
17: use Cake\Controller\Component;
18: use Cake\Event\Event;
19: use Cake\Http\Cookie\Cookie;
20: use Cake\Http\Exception\InvalidCsrfTokenException;
21: use Cake\Http\Response;
22: use Cake\Http\ServerRequest;
23: use Cake\I18n\Time;
24: use Cake\Utility\Security;
25:
26: /**
27: * Provides CSRF protection & validation.
28: *
29: * This component adds a CSRF token to a cookie. The cookie value is compared to
30: * request data, or the X-CSRF-Token header on each PATCH, POST,
31: * PUT, or DELETE request.
32: *
33: * If the request data is missing or does not match the cookie data,
34: * an InvalidCsrfTokenException will be raised.
35: *
36: * This component integrates with the FormHelper automatically and when
37: * used together your forms will have CSRF tokens automatically added
38: * when `$this->Form->create(...)` is used in a view.
39: *
40: * @deprecated 3.5.0 Use Cake\Http\Middleware\CsrfProtectionMiddleware instead.
41: */
42: class CsrfComponent extends Component
43: {
44: /**
45: * Default config for the CSRF handling.
46: *
47: * - cookieName = The name of the cookie to send.
48: * - expiry = How long the CSRF token should last. Defaults to browser session.
49: * - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false.
50: * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false.
51: * - field = The form field to check. Changing this will also require configuring
52: * FormHelper.
53: *
54: * @var array
55: */
56: protected $_defaultConfig = [
57: 'cookieName' => 'csrfToken',
58: 'expiry' => 0,
59: 'secure' => false,
60: 'httpOnly' => false,
61: 'field' => '_csrfToken',
62: ];
63:
64: /**
65: * Warn if CsrfComponent is used together with CsrfProtectionMiddleware
66: *
67: * @param array $config The config data.
68: * @return void
69: */
70: public function initialize(array $config)
71: {
72: if ($this->getController()->getRequest()->getParam('_csrfToken') !== false) {
73: deprecationWarning('Loading CsrfComponent while CsrfProtectionMiddleware is active ' .
74: 'will corrupt CSRF data and form submitting will fail.');
75: }
76: }
77:
78: /**
79: * Startup callback.
80: *
81: * Validates the CSRF token for POST data. If
82: * the request is a GET request, and the cookie value is absent a cookie will be set.
83: *
84: * Once a cookie is set it will be copied into request->getParam('_csrfToken')
85: * so that application and framework code can easily access the csrf token.
86: *
87: * RequestAction requests do not get checked, nor will
88: * they set a cookie should it be missing.
89: *
90: * @param \Cake\Event\Event $event Event instance.
91: * @return void
92: */
93: public function startup(Event $event)
94: {
95: /** @var \Cake\Controller\Controller $controller */
96: $controller = $event->getSubject();
97: $request = $controller->getRequest();
98: $response = $controller->getResponse();
99: $cookieName = $this->_config['cookieName'];
100:
101: $cookieData = $request->getCookie($cookieName);
102: if ($cookieData) {
103: $request = $request->withParam('_csrfToken', $cookieData);
104: }
105:
106: if ($request->is('requested')) {
107: $controller->setRequest($request);
108:
109: return;
110: }
111:
112: if ($request->is('get') && $cookieData === null) {
113: list($request, $response) = $this->_setCookie($request, $response);
114: $controller->setResponse($response);
115: }
116: if ($request->is(['put', 'post', 'delete', 'patch']) || $request->getData()) {
117: $this->_validateToken($request);
118: $request = $request->withoutData($this->_config['field']);
119: }
120: $controller->setRequest($request);
121: }
122:
123: /**
124: * Events supported by this component.
125: *
126: * @return array
127: */
128: public function implementedEvents()
129: {
130: return [
131: 'Controller.startup' => 'startup',
132: ];
133: }
134:
135: /**
136: * Set the cookie in the response.
137: *
138: * Also sets the request->params['_csrfToken'] so the newly minted
139: * token is available in the request data.
140: *
141: * @param \Cake\Http\ServerRequest $request The request object.
142: * @param \Cake\Http\Response $response The response object.
143: * @return array An array of the modified request, response.
144: */
145: protected function _setCookie(ServerRequest $request, Response $response)
146: {
147: $expiry = new Time($this->_config['expiry']);
148: $value = hash('sha512', Security::randomBytes(16), false);
149:
150: $request = $request->withParam('_csrfToken', $value);
151:
152: $cookie = new Cookie(
153: $this->_config['cookieName'],
154: $value,
155: $expiry,
156: $request->getAttribute('webroot'),
157: '',
158: (bool)$this->_config['secure'],
159: (bool)$this->_config['httpOnly']
160: );
161:
162: $response = $response->withCookie($cookie);
163:
164: return [$request, $response];
165: }
166:
167: /**
168: * Validate the request data against the cookie token.
169: *
170: * @param \Cake\Http\ServerRequest $request The request to validate against.
171: * @throws \Cake\Http\Exception\InvalidCsrfTokenException when the CSRF token is invalid or missing.
172: * @return void
173: */
174: protected function _validateToken(ServerRequest $request)
175: {
176: $cookie = $request->getCookie($this->_config['cookieName']);
177: $post = $request->getData($this->_config['field']);
178: $header = $request->getHeaderLine('X-CSRF-Token');
179:
180: if (!$cookie) {
181: throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
182: }
183:
184: if (!Security::constantEquals($post, $cookie) && !Security::constantEquals($header, $cookie)) {
185: throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
186: }
187: }
188: }
189: