1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:
20: App::import('Core', array('String', 'Security'));
21:
22: 23: 24: 25: 26: 27: 28:
29: class SecurityComponent extends Object {
30:
31: 32: 33: 34: 35: 36:
37: var $blackHoleCallback = null;
38:
39: 40: 41: 42: 43: 44: 45:
46: var $requirePost = array();
47:
48: 49: 50: 51: 52: 53: 54:
55: var $requireGet = array();
56:
57: 58: 59: 60: 61: 62: 63:
64: var $requirePut = array();
65:
66: 67: 68: 69: 70: 71: 72:
73: var $requireDelete = array();
74:
75: 76: 77: 78: 79: 80: 81:
82: var $requireSecure = array();
83:
84: 85: 86: 87: 88: 89: 90:
91: var $requireAuth = array();
92:
93: 94: 95: 96: 97: 98: 99:
100: var $requireLogin = array();
101:
102: 103: 104: 105: 106: 107: 108:
109: var $loginOptions = array('type' => '', 'prompt' => null);
110:
111: 112: 113: 114: 115: 116: 117:
118: var $loginUsers = array();
119:
120: 121: 122: 123: 124: 125: 126: 127:
128: var $allowedControllers = array();
129:
130: 131: 132: 133: 134: 135: 136: 137:
138: var $allowedActions = array();
139:
140: 141: 142: 143: 144: 145:
146: var $disabledFields = array();
147:
148: 149: 150: 151: 152: 153: 154:
155: var $validatePost = true;
156:
157: 158: 159: 160: 161: 162:
163: var $components = array('RequestHandler', 'Session');
164:
165: 166: 167: 168: 169:
170: var $_action = null;
171:
172: 173: 174: 175: 176: 177: 178: 179:
180: function initialize(&$controller, $settings = array()) {
181: $this->_set($settings);
182: }
183:
184: 185: 186: 187: 188: 189: 190:
191: function startup(&$controller) {
192: $this->_action = strtolower($controller->action);
193: $this->_methodsRequired($controller);
194: $this->_secureRequired($controller);
195: $this->_authRequired($controller);
196: $this->_loginRequired($controller);
197:
198: $isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
199: $isRequestAction = (
200: !isset($controller->params['requested']) ||
201: $controller->params['requested'] != 1
202: );
203:
204: if ($isPost && $isRequestAction && $this->validatePost) {
205: if ($this->_validatePost($controller) === false) {
206: if (!$this->blackHole($controller, 'auth')) {
207: return null;
208: }
209: }
210: }
211: $this->_generateToken($controller);
212: }
213:
214: 215: 216: 217: 218: 219: 220:
221: function requirePost() {
222: $args = func_get_args();
223: $this->_requireMethod('Post', $args);
224: }
225:
226: 227: 228: 229: 230: 231:
232: function requireGet() {
233: $args = func_get_args();
234: $this->_requireMethod('Get', $args);
235: }
236:
237: 238: 239: 240: 241: 242:
243: function requirePut() {
244: $args = func_get_args();
245: $this->_requireMethod('Put', $args);
246: }
247:
248: 249: 250: 251: 252: 253:
254: function requireDelete() {
255: $args = func_get_args();
256: $this->_requireMethod('Delete', $args);
257: }
258:
259: 260: 261: 262: 263: 264: 265:
266: function requireSecure() {
267: $args = func_get_args();
268: $this->_requireMethod('Secure', $args);
269: }
270:
271: 272: 273: 274: 275: 276: 277:
278: function requireAuth() {
279: $args = func_get_args();
280: $this->_requireMethod('Auth', $args);
281: }
282:
283: 284: 285: 286: 287: 288: 289:
290: function requireLogin() {
291: $args = func_get_args();
292: $base = $this->loginOptions;
293:
294: foreach ($args as $i => $arg) {
295: if (is_array($arg)) {
296: $this->loginOptions = $arg;
297: unset($args[$i]);
298: }
299: }
300: $this->loginOptions = array_merge($base, $this->loginOptions);
301: $this->_requireMethod('Login', $args);
302:
303: if (isset($this->loginOptions['users'])) {
304: $this->loginUsers =& $this->loginOptions['users'];
305: }
306: }
307:
308: 309: 310: 311: 312: 313: 314: 315:
316: function loginCredentials($type = null) {
317: switch (strtolower($type)) {
318: case 'basic':
319: $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW'));
320: if (!empty($login['username'])) {
321: return $login;
322: }
323: break;
324: case 'digest':
325: default:
326: $digest = null;
327:
328: if (version_compare(PHP_VERSION, '5.1') != -1) {
329: $digest = env('PHP_AUTH_DIGEST');
330: } elseif (function_exists('apache_request_headers')) {
331: $headers = apache_request_headers();
332: if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
333: $digest = substr($headers['Authorization'], 7);
334: }
335: } else {
336:
337: trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING);
338: }
339:
340: if (!empty($digest)) {
341: return $this->parseDigestAuthData($digest);
342: }
343: break;
344: }
345: return null;
346: }
347:
348: 349: 350: 351: 352: 353: 354: 355:
356: function loginRequest($options = array()) {
357: $options = array_merge($this->loginOptions, $options);
358: $this->_setLoginDefaults($options);
359: $auth = 'WWW-Authenticate: ' . ucfirst($options['type']);
360: $out = array('realm="' . $options['realm'] . '"');
361:
362: if (strtolower($options['type']) == 'digest') {
363: $out[] = 'qop="auth"';
364: $out[] = 'nonce="' . uniqid("") . '"';
365: $out[] = 'opaque="' . md5($options['realm']) . '"';
366: }
367:
368: return $auth . ' ' . implode(',', $out);
369: }
370:
371: 372: 373: 374: 375: 376: 377: 378:
379: function parseDigestAuthData($digest) {
380: if (substr($digest, 0, 7) == 'Digest ') {
381: $digest = substr($digest, 7);
382: }
383: $keys = array();
384: $match = array();
385: $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
386: preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9@=.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
387:
388: foreach ($match as $i) {
389: $keys[$i[1]] = $i[3];
390: unset($req[$i[1]]);
391: }
392:
393: if (empty($req)) {
394: return $keys;
395: }
396: return null;
397: }
398:
399: 400: 401: 402: 403: 404: 405: 406: 407:
408: function generateDigestResponseHash($data) {
409: return md5(
410: md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) .
411: ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' .
412: md5(env('REQUEST_METHOD') . ':' . $data['uri'])
413: );
414: }
415:
416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426:
427: function blackHole(&$controller, $error = '') {
428: if ($this->blackHoleCallback == null) {
429: $code = 404;
430: if ($error == 'login') {
431: $code = 401;
432: $controller->header($this->loginRequest());
433: }
434: $controller->redirect(null, $code, true);
435: } else {
436: return $this->_callback($controller, $this->blackHoleCallback, array($error));
437: }
438: }
439:
440: 441: 442: 443: 444: 445: 446: 447:
448: function _requireMethod($method, $actions = array()) {
449: if (isset($actions[0]) && is_array($actions[0])) {
450: $actions = $actions[0];
451: }
452: $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
453: }
454:
455: 456: 457: 458: 459: 460: 461:
462: function _methodsRequired(&$controller) {
463: foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
464: $property = 'require' . $method;
465: if (is_array($this->$property) && !empty($this->$property)) {
466: $require = array_map('strtolower', $this->$property);
467:
468: if (in_array($this->_action, $require) || $this->$property == array('*')) {
469: if (!$this->RequestHandler->{'is' . $method}()) {
470: if (!$this->blackHole($controller, strtolower($method))) {
471: return null;
472: }
473: }
474: }
475: }
476: }
477: return true;
478: }
479:
480: 481: 482: 483: 484: 485: 486:
487: function _secureRequired(&$controller) {
488: if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
489: $requireSecure = array_map('strtolower', $this->requireSecure);
490:
491: if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
492: if (!$this->RequestHandler->isSSL()) {
493: if (!$this->blackHole($controller, 'secure')) {
494: return null;
495: }
496: }
497: }
498: }
499: return true;
500: }
501:
502: 503: 504: 505: 506: 507: 508:
509: function _authRequired(&$controller) {
510: if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->data)) {
511: $requireAuth = array_map('strtolower', $this->requireAuth);
512:
513: if (in_array($this->_action, $requireAuth) || $this->requireAuth == array('*')) {
514: if (!isset($controller->data['_Token'] )) {
515: if (!$this->blackHole($controller, 'auth')) {
516: return null;
517: }
518: }
519:
520: if ($this->Session->check('_Token')) {
521: $tData = unserialize($this->Session->read('_Token'));
522:
523: if (!empty($tData['allowedControllers']) && !in_array($controller->params['controller'], $tData['allowedControllers']) || !empty($tData['allowedActions']) && !in_array($controller->params['action'], $tData['allowedActions'])) {
524: if (!$this->blackHole($controller, 'auth')) {
525: return null;
526: }
527: }
528: } else {
529: if (!$this->blackHole($controller, 'auth')) {
530: return null;
531: }
532: }
533: }
534: }
535: return true;
536: }
537:
538: 539: 540: 541: 542: 543: 544:
545: function _loginRequired(&$controller) {
546: if (is_array($this->requireLogin) && !empty($this->requireLogin)) {
547: $requireLogin = array_map('strtolower', $this->requireLogin);
548:
549: if (in_array($this->_action, $requireLogin) || $this->requireLogin == array('*')) {
550: $login = $this->loginCredentials($this->loginOptions['type']);
551:
552: if ($login == null) {
553: $controller->header($this->loginRequest());
554:
555: if (!empty($this->loginOptions['prompt'])) {
556: $this->_callback($controller, $this->loginOptions['prompt']);
557: } else {
558: $this->blackHole($controller, 'login');
559: }
560: } else {
561: if (isset($this->loginOptions['login'])) {
562: $this->_callback($controller, $this->loginOptions['login'], array($login));
563: } else {
564: if (strtolower($this->loginOptions['type']) == 'digest') {
565: if ($login && isset($this->loginUsers[$login['username']])) {
566: if ($login['response'] == $this->generateDigestResponseHash($login)) {
567: return true;
568: }
569: }
570: $this->blackHole($controller, 'login');
571: } else {
572: if (
573: !(in_array($login['username'], array_keys($this->loginUsers)) &&
574: $this->loginUsers[$login['username']] == $login['password'])
575: ) {
576: $this->blackHole($controller, 'login');
577: }
578: }
579: }
580: }
581: }
582: }
583: return true;
584: }
585:
586: 587: 588: 589: 590: 591: 592:
593: function _validatePost(&$controller) {
594: if (empty($controller->data)) {
595: return true;
596: }
597: $data = $controller->data;
598:
599: if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['key'])) {
600: return false;
601: }
602: $token = $data['_Token']['key'];
603:
604: if ($this->Session->check('_Token')) {
605: $tokenData = unserialize($this->Session->read('_Token'));
606:
607: if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
608: return false;
609: }
610: } else {
611: return false;
612: }
613:
614: $locked = null;
615: $check = $controller->data;
616: $token = urldecode($check['_Token']['fields']);
617:
618: if (strpos($token, ':')) {
619: list($token, $locked) = explode(':', $token, 2);
620: }
621: unset($check['_Token']);
622:
623: $locked = explode('|', $locked);
624:
625: $lockedFields = array();
626: $fields = Set::flatten($check);
627: $fieldList = array_keys($fields);
628: $multi = array();
629:
630: foreach ($fieldList as $i => $key) {
631: if (preg_match('/\.\d+$/', $key)) {
632: $multi[$i] = preg_replace('/\.\d+$/', '', $key);
633: unset($fieldList[$i]);
634: }
635: }
636: if (!empty($multi)) {
637: $fieldList += array_unique($multi);
638: }
639:
640: foreach ($fieldList as $i => $key) {
641: $isDisabled = false;
642: $isLocked = (is_array($locked) && in_array($key, $locked));
643:
644: if (!empty($this->disabledFields)) {
645: foreach ((array)$this->disabledFields as $disabled) {
646: $disabled = explode('.', $disabled);
647: $field = array_values(array_intersect(explode('.', $key), $disabled));
648: $isDisabled = ($field === $disabled);
649: if ($isDisabled) {
650: break;
651: }
652: }
653: }
654:
655: if ($isDisabled || $isLocked) {
656: unset($fieldList[$i]);
657: if ($isLocked) {
658: $lockedFields[$key] = $fields[$key];
659: }
660: }
661: }
662: sort($fieldList, SORT_STRING);
663: ksort($lockedFields, SORT_STRING);
664:
665: $fieldList += $lockedFields;
666: $url = $controller->here;
667: $check = Security::hash($url . serialize($fieldList) . Configure::read('Security.salt'));
668: return ($token === $check);
669: }
670:
671: 672: 673: 674: 675: 676: 677:
678: function _generateToken(&$controller) {
679: if (isset($controller->params['requested']) && $controller->params['requested'] === 1) {
680: if ($this->Session->check('_Token')) {
681: $tokenData = unserialize($this->Session->read('_Token'));
682: $controller->params['_Token'] = $tokenData;
683: }
684: return false;
685: }
686: $authKey = Security::generateAuthKey();
687: $expires = strtotime('+' . Security::inactiveMins() . ' minutes');
688: $token = array(
689: 'key' => $authKey,
690: 'expires' => $expires,
691: 'allowedControllers' => $this->allowedControllers,
692: 'allowedActions' => $this->allowedActions,
693: 'disabledFields' => $this->disabledFields
694: );
695:
696: if (!isset($controller->data)) {
697: $controller->data = array();
698: }
699:
700: if ($this->Session->check('_Token')) {
701: $tokenData = unserialize($this->Session->read('_Token'));
702: $valid = (
703: isset($tokenData['expires']) &&
704: $tokenData['expires'] > time() &&
705: isset($tokenData['key'])
706: );
707:
708: if ($valid) {
709: $token['key'] = $tokenData['key'];
710: }
711: }
712: $controller->params['_Token'] = $token;
713: $this->Session->write('_Token', serialize($token));
714: return true;
715: }
716:
717: 718: 719: 720: 721: 722: 723:
724: function _setLoginDefaults(&$options) {
725: $options = array_merge(array(
726: 'type' => 'basic',
727: 'realm' => env('SERVER_NAME'),
728: 'qop' => 'auth',
729: 'nonce' => String::uuid()
730: ), array_filter($options));
731: $options = array_merge(array('opaque' => md5($options['realm'])), $options);
732: }
733:
734: 735: 736: 737: 738: 739: 740: 741: 742:
743: function _callback(&$controller, $method, $params = array()) {
744: if (is_callable(array($controller, $method))) {
745: return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
746: } else {
747: return null;
748: }
749: }
750: }
751: