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