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.0.0
13: * @license http://www.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\I18n\Time;
20: use Cake\Network\Exception\InvalidCsrfTokenException;
21: use Cake\Network\Request;
22: use Cake\Network\Response;
23: use Cake\Utility\Security;
24:
25: /**
26: * Provides CSRF protection & validation.
27: *
28: * This component 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 component 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 CsrfComponent extends Component
40: {
41:
42: /**
43: * Default config for the CSRF handling.
44: *
45: * - cookieName = The name of the cookie to send.
46: * - expiry = How long the CSRF token should last. 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: * Startup callback.
64: *
65: * Validates the CSRF token for POST data. If
66: * the request is a GET request, and the cookie value is absent a cookie will be set.
67: *
68: * Once a cookie is set it will be copied into request->params['_csrfToken']
69: * so that application and framework code can easily access the csrf token.
70: *
71: * RequestAction requests do not get checked, nor will
72: * they set a cookie should it be missing.
73: *
74: * @param \Cake\Event\Event $event Event instance.
75: * @return void
76: */
77: public function startup(Event $event)
78: {
79: $controller = $event->subject();
80: $request = $controller->request;
81: $response = $controller->response;
82: $cookieName = $this->_config['cookieName'];
83:
84: $cookieData = $request->cookie($cookieName);
85: if ($cookieData) {
86: $request->params['_csrfToken'] = $cookieData;
87: }
88:
89: if ($request->is('requested')) {
90: return;
91: }
92:
93: if ($request->is('get') && $cookieData === null) {
94: $this->_setCookie($request, $response);
95: }
96: if ($request->is(['put', 'post', 'delete', 'patch']) || !empty($request->data)) {
97: $this->_validateToken($request);
98: unset($request->data[$this->_config['field']]);
99: }
100: }
101:
102: /**
103: * Events supported by this component.
104: *
105: * @return array
106: */
107: public function implementedEvents()
108: {
109: return [
110: 'Controller.startup' => 'startup',
111: ];
112: }
113:
114: /**
115: * Set the cookie in the response.
116: *
117: * Also sets the request->params['_csrfToken'] so the newly minted
118: * token is available in the request data.
119: *
120: * @param \Cake\Network\Request $request The request object.
121: * @param \Cake\Network\Response $response The response object.
122: * @return void
123: */
124: protected function _setCookie(Request $request, Response $response)
125: {
126: $expiry = new Time($this->_config['expiry']);
127: $value = hash('sha512', Security::randomBytes(16), false);
128:
129: $request->params['_csrfToken'] = $value;
130: $response->cookie([
131: 'name' => $this->_config['cookieName'],
132: 'value' => $value,
133: 'expire' => $expiry->format('U'),
134: 'path' => $request->webroot,
135: 'secure' => $this->_config['secure'],
136: 'httpOnly' => $this->_config['httpOnly'],
137: ]);
138: }
139:
140: /**
141: * Validate the request data against the cookie token.
142: *
143: * @param \Cake\Network\Request $request The request to validate against.
144: * @throws \Cake\Network\Exception\InvalidCsrfTokenException when the CSRF token is invalid or missing.
145: * @return void
146: */
147: protected function _validateToken(Request $request)
148: {
149: $cookie = $request->cookie($this->_config['cookieName']);
150: $post = $request->data($this->_config['field']);
151: $header = $request->header('X-CSRF-Token');
152:
153: if (empty($cookie)) {
154: throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
155: }
156:
157: if ($post !== $cookie && $header !== $cookie) {
158: throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
159: }
160: }
161: }
162: