1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
19:
20: App::uses('Component', 'Controller');
21: App::uses('String', 'Utility');
22: App::uses('Security', 'Utility');
23:
24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37: class SecurityComponent extends Component {
38:
39: 40: 41: 42: 43:
44: public $blackHoleCallback = null;
45:
46: 47: 48: 49: 50: 51:
52: public $requirePost = array();
53:
54: 55: 56: 57: 58: 59:
60: public $requireGet = array();
61:
62: 63: 64: 65: 66: 67:
68: public $requirePut = array();
69:
70: 71: 72: 73: 74: 75:
76: public $requireDelete = array();
77:
78: 79: 80: 81: 82: 83:
84: public $requireSecure = array();
85:
86: 87: 88: 89: 90: 91:
92: public $requireAuth = array();
93:
94: 95: 96: 97: 98: 99: 100:
101: public $allowedControllers = array();
102:
103: 104: 105: 106: 107: 108: 109:
110: public $allowedActions = array();
111:
112: 113: 114: 115: 116: 117: 118:
119: public $disabledFields = array();
120:
121: 122: 123: 124: 125: 126: 127: 128:
129: public $unlockedFields = array();
130:
131: 132: 133: 134: 135: 136:
137: public $validatePost = true;
138:
139: 140: 141: 142: 143: 144: 145:
146: public $csrfCheck = true;
147:
148: 149: 150: 151: 152: 153: 154:
155: public $csrfExpires = '+30 minutes';
156:
157: 158: 159: 160: 161: 162: 163: 164:
165: public $csrfUseOnce = true;
166:
167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177:
178: public $csrfLimit = 100;
179:
180: 181: 182: 183: 184:
185: public $components = array('Session');
186:
187: 188: 189: 190: 191:
192: protected $_action = null;
193:
194: 195: 196: 197: 198:
199: public $request;
200:
201: 202: 203: 204: 205: 206:
207: public function startup(Controller $controller) {
208: $this->request = $controller->request;
209: $this->_action = $this->request->params['action'];
210: $this->_methodsRequired($controller);
211: $this->_secureRequired($controller);
212: $this->_authRequired($controller);
213:
214: $isPost = ($this->request->is('post') || $this->request->is('put'));
215: $isNotRequestAction = (
216: !isset($controller->request->params['requested']) ||
217: $controller->request->params['requested'] != 1
218: );
219:
220: if ($isPost && $isNotRequestAction && $this->validatePost) {
221: if ($this->_validatePost($controller) === false) {
222: return $this->blackHole($controller, 'auth');
223: }
224: }
225: if ($isPost && $isNotRequestAction && $this->csrfCheck) {
226: if ($this->_validateCsrf($controller) === false) {
227: return $this->blackHole($controller, 'csrf');
228: }
229: }
230: $this->generateToken($controller->request);
231: if ($isPost) {
232: unset($controller->request->data['_Token']);
233: }
234: }
235:
236: 237: 238: 239: 240: 241:
242: public function requirePost() {
243: $args = func_get_args();
244: $this->_requireMethod('Post', $args);
245: }
246:
247: 248: 249: 250: 251:
252: public function requireGet() {
253: $args = func_get_args();
254: $this->_requireMethod('Get', $args);
255: }
256:
257: 258: 259: 260: 261:
262: public function requirePut() {
263: $args = func_get_args();
264: $this->_requireMethod('Put', $args);
265: }
266:
267: 268: 269: 270: 271:
272: public function requireDelete() {
273: $args = func_get_args();
274: $this->_requireMethod('Delete', $args);
275: }
276:
277: 278: 279: 280: 281: 282:
283: public function requireSecure() {
284: $args = func_get_args();
285: $this->_requireMethod('Secure', $args);
286: }
287:
288: 289: 290: 291: 292: 293:
294: public function requireAuth() {
295: $args = func_get_args();
296: $this->_requireMethod('Auth', $args);
297: }
298:
299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309:
310: public function blackHole(Controller $controller, $error = '') {
311: if ($this->blackHoleCallback == null) {
312: throw new BadRequestException(__d('cake_dev', 'The request has been black-holed'));
313: } else {
314: return $this->_callback($controller, $this->blackHoleCallback, array($error));
315: }
316: }
317:
318: 319: 320: 321: 322: 323: 324:
325: protected function _requireMethod($method, $actions = array()) {
326: if (isset($actions[0]) && is_array($actions[0])) {
327: $actions = $actions[0];
328: }
329: $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
330: }
331:
332: 333: 334: 335: 336: 337:
338: protected function _methodsRequired(Controller $controller) {
339: foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
340: $property = 'require' . $method;
341: if (is_array($this->$property) && !empty($this->$property)) {
342: $require = $this->$property;
343: if (in_array($this->_action, $require) || $this->$property == array('*')) {
344: if (!$this->request->is($method)) {
345: if (!$this->blackHole($controller, $method)) {
346: return null;
347: }
348: }
349: }
350: }
351: }
352: return true;
353: }
354:
355: 356: 357: 358: 359: 360:
361: protected function _secureRequired(Controller $controller) {
362: if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
363: $requireSecure = $this->requireSecure;
364:
365: if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
366: if (!$this->request->is('ssl')) {
367: if (!$this->blackHole($controller, 'secure')) {
368: return null;
369: }
370: }
371: }
372: }
373: return true;
374: }
375:
376: 377: 378: 379: 380: 381:
382: protected function _authRequired(Controller $controller) {
383: if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($this->request->data)) {
384: $requireAuth = $this->requireAuth;
385:
386: if (in_array($this->request->params['action'], $requireAuth) || $this->requireAuth == array('*')) {
387: if (!isset($controller->request->data['_Token'] )) {
388: if (!$this->blackHole($controller, 'auth')) {
389: return null;
390: }
391: }
392:
393: if ($this->Session->check('_Token')) {
394: $tData = $this->Session->read('_Token');
395:
396: if (
397: !empty($tData['allowedControllers']) &&
398: !in_array($this->request->params['controller'], $tData['allowedControllers']) ||
399: !empty($tData['allowedActions']) &&
400: !in_array($this->request->params['action'], $tData['allowedActions'])
401: ) {
402: if (!$this->blackHole($controller, 'auth')) {
403: return null;
404: }
405: }
406: } else {
407: if (!$this->blackHole($controller, 'auth')) {
408: return null;
409: }
410: }
411: }
412: }
413: return true;
414: }
415:
416: 417: 418: 419: 420: 421:
422: protected function _validatePost(Controller $controller) {
423: if (empty($controller->request->data)) {
424: return true;
425: }
426: $data = $controller->request->data;
427:
428: if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['unlocked'])) {
429: return false;
430: }
431:
432: $locked = '';
433: $check = $controller->request->data;
434: $token = urldecode($check['_Token']['fields']);
435: $unlocked = urldecode($check['_Token']['unlocked']);
436:
437: if (strpos($token, ':')) {
438: list($token, $locked) = explode(':', $token, 2);
439: }
440: unset($check['_Token']);
441:
442: $locked = explode('|', $locked);
443: $unlocked = explode('|', $unlocked);
444:
445: $lockedFields = array();
446: $fields = Set::flatten($check);
447: $fieldList = array_keys($fields);
448: $multi = array();
449:
450: foreach ($fieldList as $i => $key) {
451: if (preg_match('/(\.\d+)+$/', $key)) {
452: $multi[$i] = preg_replace('/(\.\d+)+$/', '', $key);
453: unset($fieldList[$i]);
454: }
455: }
456: if (!empty($multi)) {
457: $fieldList += array_unique($multi);
458: }
459:
460: $unlockedFields = array_unique(
461: array_merge((array)$this->disabledFields, (array)$this->unlockedFields, $unlocked)
462: );
463:
464: foreach ($fieldList as $i => $key) {
465: $isLocked = (is_array($locked) && in_array($key, $locked));
466:
467: if (!empty($unlockedFields)) {
468: foreach ($unlockedFields as $off) {
469: $off = explode('.', $off);
470: $field = array_values(array_intersect(explode('.', $key), $off));
471: $isUnlocked = ($field === $off);
472: if ($isUnlocked) {
473: break;
474: }
475: }
476: }
477:
478: if ($isUnlocked || $isLocked) {
479: unset($fieldList[$i]);
480: if ($isLocked) {
481: $lockedFields[$key] = $fields[$key];
482: }
483: }
484: }
485: sort($unlocked, SORT_STRING);
486: sort($fieldList, SORT_STRING);
487: ksort($lockedFields, SORT_STRING);
488:
489: $fieldList += $lockedFields;
490: $unlocked = implode('|', $unlocked);
491: $check = Security::hash(serialize($fieldList) . $unlocked . Configure::read('Security.salt'));
492: return ($token === $check);
493: }
494:
495: 496: 497: 498: 499: 500:
501: public function generateToken(CakeRequest $request) {
502: if (isset($request->params['requested']) && $request->params['requested'] === 1) {
503: if ($this->Session->check('_Token')) {
504: $request->params['_Token'] = $this->Session->read('_Token');
505: }
506: return false;
507: }
508: $authKey = Security::generateAuthKey();
509: $token = array(
510: 'key' => $authKey,
511: 'allowedControllers' => $this->allowedControllers,
512: 'allowedActions' => $this->allowedActions,
513: 'unlockedFields' => array_merge($this->disabledFields, $this->unlockedFields),
514: 'csrfTokens' => array()
515: );
516:
517: $tokenData = array();
518: if ($this->Session->check('_Token')) {
519: $tokenData = $this->Session->read('_Token');
520: if (!empty($tokenData['csrfTokens']) && is_array($tokenData['csrfTokens'])) {
521: $token['csrfTokens'] = $this->_expireTokens($tokenData['csrfTokens']);
522: }
523: }
524: if ($this->csrfUseOnce || empty($token['csrfTokens'])) {
525: $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires);
526: }
527: if (!$this->csrfUseOnce) {
528: $csrfTokens = array_keys($token['csrfTokens']);
529: $token['key'] = $csrfTokens[0];
530: }
531: $this->Session->write('_Token', $token);
532: $request->params['_Token'] = array(
533: 'key' => $token['key'],
534: 'unlockedFields' => $token['unlockedFields']
535: );
536: return true;
537: }
538:
539: 540: 541: 542: 543: 544: 545: 546:
547: protected function _validateCsrf(Controller $controller) {
548: $token = $this->Session->read('_Token');
549: $requestToken = $controller->request->data('_Token.key');
550: if (isset($token['csrfTokens'][$requestToken]) && $token['csrfTokens'][$requestToken] >= time()) {
551: if ($this->csrfUseOnce) {
552: $this->Session->delete('_Token.csrfTokens.' . $requestToken);
553: }
554: return true;
555: }
556: return false;
557: }
558:
559: 560: 561: 562: 563: 564: 565:
566: protected function _expireTokens($tokens) {
567: $now = time();
568: foreach ($tokens as $nonce => $expires) {
569: if ($expires < $now) {
570: unset($tokens[$nonce]);
571: }
572: }
573: $overflow = count($tokens) - $this->csrfLimit;
574: if ($overflow > 0) {
575: $tokens = array_slice($tokens, $overflow + 1, null, true);
576: }
577: return $tokens;
578: }
579:
580: 581: 582: 583: 584: 585: 586: 587:
588: protected function _callback(Controller $controller, $method, $params = array()) {
589: if (is_callable(array($controller, $method))) {
590: return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
591: } else {
592: return null;
593: }
594: }
595:
596: }
597: