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