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: public $components = array('Session');
173:
174: 175: 176: 177: 178:
179: protected $_action = null;
180:
181: 182: 183: 184: 185:
186: public $request;
187:
188: 189: 190: 191: 192: 193:
194: public function startup($controller) {
195: $this->request = $controller->request;
196: $this->_action = $this->request->params['action'];
197: $this->_methodsRequired($controller);
198: $this->_secureRequired($controller);
199: $this->_authRequired($controller);
200:
201: $isPost = ($this->request->is('post') || $this->request->is('put'));
202: $isNotRequestAction = (
203: !isset($controller->request->params['requested']) ||
204: $controller->request->params['requested'] != 1
205: );
206:
207: if ($isPost && $isNotRequestAction && $this->validatePost) {
208: if ($this->_validatePost($controller) === false) {
209: return $this->blackHole($controller, 'auth');
210: }
211: }
212: if ($isPost && $isNotRequestAction && $this->csrfCheck) {
213: if ($this->_validateCsrf($controller) === false) {
214: return $this->blackHole($controller, 'csrf');
215: }
216: }
217: $this->_generateToken($controller);
218: if ($isPost) {
219: unset($controller->request->data['_Token']);
220: }
221: }
222:
223: 224: 225: 226: 227: 228:
229: public function requirePost() {
230: $args = func_get_args();
231: $this->_requireMethod('Post', $args);
232: }
233:
234: 235: 236: 237: 238:
239: public function requireGet() {
240: $args = func_get_args();
241: $this->_requireMethod('Get', $args);
242: }
243:
244: 245: 246: 247: 248:
249: public function requirePut() {
250: $args = func_get_args();
251: $this->_requireMethod('Put', $args);
252: }
253:
254: 255: 256: 257: 258:
259: public function requireDelete() {
260: $args = func_get_args();
261: $this->_requireMethod('Delete', $args);
262: }
263:
264: 265: 266: 267: 268: 269:
270: public function requireSecure() {
271: $args = func_get_args();
272: $this->_requireMethod('Secure', $args);
273: }
274:
275: 276: 277: 278: 279: 280:
281: public function requireAuth() {
282: $args = func_get_args();
283: $this->_requireMethod('Auth', $args);
284: }
285:
286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296:
297: public function blackHole($controller, $error = '') {
298: if ($this->blackHoleCallback == null) {
299: throw new BadRequestException(__d('cake_dev', 'The request has been black-holed'));
300: } else {
301: return $this->_callback($controller, $this->blackHoleCallback, array($error));
302: }
303: }
304:
305: 306: 307: 308: 309: 310: 311:
312: protected function _requireMethod($method, $actions = array()) {
313: if (isset($actions[0]) && is_array($actions[0])) {
314: $actions = $actions[0];
315: }
316: $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
317: }
318:
319: 320: 321: 322: 323: 324:
325: protected function _methodsRequired($controller) {
326: foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
327: $property = 'require' . $method;
328: if (is_array($this->$property) && !empty($this->$property)) {
329: $require = $this->$property;
330: if (in_array($this->_action, $require) || $this->$property == array('*')) {
331: if (!$this->request->is($method)) {
332: if (!$this->blackHole($controller, $method)) {
333: return null;
334: }
335: }
336: }
337: }
338: }
339: return true;
340: }
341:
342: 343: 344: 345: 346: 347:
348: protected function _secureRequired($controller) {
349: if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
350: $requireSecure = $this->requireSecure;
351:
352: if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
353: if (!$this->request->is('ssl')) {
354: if (!$this->blackHole($controller, 'secure')) {
355: return null;
356: }
357: }
358: }
359: }
360: return true;
361: }
362:
363: 364: 365: 366: 367: 368:
369: protected function _authRequired($controller) {
370: if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($this->request->data)) {
371: $requireAuth = $this->requireAuth;
372:
373: if (in_array($this->request->params['action'], $requireAuth) || $this->requireAuth == array('*')) {
374: if (!isset($controller->request->data['_Token'] )) {
375: if (!$this->blackHole($controller, 'auth')) {
376: return null;
377: }
378: }
379:
380: if ($this->Session->check('_Token')) {
381: $tData = $this->Session->read('_Token');
382:
383: if (
384: !empty($tData['allowedControllers']) &&
385: !in_array($this->request->params['controller'], $tData['allowedControllers']) ||
386: !empty($tData['allowedActions']) &&
387: !in_array($this->request->params['action'], $tData['allowedActions'])
388: ) {
389: if (!$this->blackHole($controller, 'auth')) {
390: return null;
391: }
392: }
393: } else {
394: if (!$this->blackHole($controller, 'auth')) {
395: return null;
396: }
397: }
398: }
399: }
400: return true;
401: }
402:
403: 404: 405: 406: 407: 408:
409: protected function _validatePost($controller) {
410: if (empty($controller->request->data)) {
411: return true;
412: }
413: $data = $controller->request->data;
414:
415: if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['unlocked'])) {
416: return false;
417: }
418:
419: $locked = '';
420: $check = $controller->request->data;
421: $token = urldecode($check['_Token']['fields']);
422: $unlocked = urldecode($check['_Token']['unlocked']);
423:
424: if (strpos($token, ':')) {
425: list($token, $locked) = explode(':', $token, 2);
426: }
427: unset($check['_Token']);
428:
429: $locked = explode('|', $locked);
430: $unlocked = explode('|', $unlocked);
431:
432: $lockedFields = array();
433: $fields = Set::flatten($check);
434: $fieldList = array_keys($fields);
435: $multi = array();
436:
437: foreach ($fieldList as $i => $key) {
438: if (preg_match('/(\.\d+)+$/', $key)) {
439: $multi[$i] = preg_replace('/(\.\d+)+$/', '', $key);
440: unset($fieldList[$i]);
441: }
442: }
443: if (!empty($multi)) {
444: $fieldList += array_unique($multi);
445: }
446:
447: $unlockedFields = array_unique(
448: array_merge((array)$this->disabledFields, (array)$this->unlockedFields, $unlocked)
449: );
450:
451: foreach ($fieldList as $i => $key) {
452: $isDisabled = false;
453: $isLocked = (is_array($locked) && in_array($key, $locked));
454:
455: if (!empty($unlockedFields)) {
456: foreach ($unlockedFields as $off) {
457: $off = explode('.', $off);
458: $field = array_values(array_intersect(explode('.', $key), $off));
459: $isUnlocked = ($field === $off);
460: if ($isUnlocked) {
461: break;
462: }
463: }
464: }
465:
466: if ($isUnlocked || $isLocked) {
467: unset($fieldList[$i]);
468: if ($isLocked) {
469: $lockedFields[$key] = $fields[$key];
470: }
471: }
472: }
473: sort($unlocked, SORT_STRING);
474: sort($fieldList, SORT_STRING);
475: ksort($lockedFields, SORT_STRING);
476:
477: $fieldList += $lockedFields;
478: $unlocked = implode('|', $unlocked);
479: $check = Security::hash(serialize($fieldList) . $unlocked . Configure::read('Security.salt'));
480: return ($token === $check);
481: }
482:
483: 484: 485: 486: 487: 488:
489: protected function _generateToken($controller) {
490: if (isset($controller->request->params['requested']) && $controller->request->params['requested'] === 1) {
491: if ($this->Session->check('_Token')) {
492: $tokenData = $this->Session->read('_Token');
493: $controller->request->params['_Token'] = $tokenData;
494: }
495: return false;
496: }
497: $authKey = Security::generateAuthKey();
498: $token = array(
499: 'key' => $authKey,
500: 'allowedControllers' => $this->allowedControllers,
501: 'allowedActions' => $this->allowedActions,
502: 'unlockedFields' => array_merge($this->disabledFields, $this->unlockedFields),
503: 'csrfTokens' => array()
504: );
505:
506: $tokenData = array();
507: if ($this->Session->check('_Token')) {
508: $tokenData = $this->Session->read('_Token');
509: if (!empty($tokenData['csrfTokens']) && is_array($tokenData['csrfTokens'])) {
510: $token['csrfTokens'] = $this->_expireTokens($tokenData['csrfTokens']);
511: }
512: }
513: if ($this->csrfCheck && ($this->csrfUseOnce || empty($token['csrfTokens'])) ) {
514: $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires);
515: }
516: if ($this->csrfCheck && $this->csrfUseOnce == false) {
517: $csrfTokens = array_keys($token['csrfTokens']);
518: $token['key'] = $csrfTokens[0];
519: }
520: $this->Session->write('_Token', $token);
521: $controller->request->params['_Token'] = array(
522: 'key' => $token['key'],
523: 'unlockedFields' => $token['unlockedFields']
524: );
525: return true;
526: }
527:
528: 529: 530: 531: 532: 533: 534: 535:
536: protected function _validateCsrf($controller) {
537: $token = $this->Session->read('_Token');
538: $requestToken = $controller->request->data('_Token.key');
539: if (isset($token['csrfTokens'][$requestToken]) && $token['csrfTokens'][$requestToken] >= time()) {
540: if ($this->csrfUseOnce) {
541: $this->Session->delete('_Token.csrfTokens.' . $requestToken);
542: }
543: return true;
544: }
545: return false;
546: }
547:
548: 549: 550: 551: 552: 553: 554:
555: protected function _expireTokens($tokens) {
556: $now = time();
557: foreach ($tokens as $nonce => $expires) {
558: if ($expires < $now) {
559: unset($tokens[$nonce]);
560: }
561: }
562: return $tokens;
563: }
564:
565: 566: 567: 568: 569: 570: 571: 572:
573: protected function _callback($controller, $method, $params = array()) {
574: if (is_callable(array($controller, $method))) {
575: return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
576: } else {
577: return null;
578: }
579: }
580: }
581: