SecurityComponentTest.php 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 1.2.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Controller\Component;
  16. use Cake\Controller\Component\SecurityComponent;
  17. use Cake\Controller\Controller;
  18. use Cake\Controller\Exception\SecurityException;
  19. use Cake\Core\Configure;
  20. use Cake\Event\Event;
  21. use Cake\Http\Response;
  22. use Cake\Http\ServerRequest;
  23. use Cake\Http\Session;
  24. use Cake\Routing\Router;
  25. use Cake\TestSuite\TestCase;
  26. use Cake\Utility\Security;
  27. /**
  28. * TestSecurityComponent
  29. */
  30. class TestSecurityComponent extends SecurityComponent
  31. {
  32. /**
  33. * validatePost method
  34. *
  35. * @param Controller $controller
  36. * @return bool
  37. */
  38. public function validatePost(Controller $controller)
  39. {
  40. return $this->_validatePost($controller);
  41. }
  42. /**
  43. * authRequired method
  44. *
  45. * @param Controller $controller
  46. * @return bool
  47. */
  48. public function authRequired(Controller $controller)
  49. {
  50. return $this->_authRequired($controller);
  51. }
  52. }
  53. /**
  54. * SecurityTestController
  55. */
  56. class SecurityTestController extends Controller
  57. {
  58. /**
  59. * components property
  60. *
  61. * @var array
  62. */
  63. public $components = [
  64. 'TestSecurity' => ['className' => 'Cake\Test\TestCase\Controller\Component\TestSecurityComponent'],
  65. ];
  66. /**
  67. * failed property
  68. *
  69. * @var bool
  70. */
  71. public $failed = false;
  72. /**
  73. * Used for keeping track of headers in test
  74. *
  75. * @var array
  76. */
  77. public $testHeaders = [];
  78. /**
  79. * fail method
  80. *
  81. * @return void
  82. */
  83. public function fail()
  84. {
  85. $this->failed = true;
  86. }
  87. /**
  88. * redirect method
  89. *
  90. * @param string|array $url
  91. * @param mixed $status
  92. * @param mixed $exit
  93. * @return void
  94. */
  95. public function redirect($url, $status = null, $exit = true)
  96. {
  97. return $status;
  98. }
  99. /**
  100. * Convenience method for header()
  101. *
  102. * @param string $status
  103. * @return void
  104. */
  105. public function header($status)
  106. {
  107. $this->testHeaders[] = $status;
  108. }
  109. }
  110. /**
  111. * SecurityComponentTest class
  112. *
  113. * @property SecurityComponent Security
  114. * @property SecurityTestController Controller
  115. */
  116. class SecurityComponentTest extends TestCase
  117. {
  118. /**
  119. * SERVER variable backup.
  120. *
  121. * @var array
  122. */
  123. protected $server = [];
  124. /**
  125. * Controller property
  126. *
  127. * @var SecurityTestController
  128. */
  129. public $Controller;
  130. /**
  131. * oldSalt property
  132. *
  133. * @var string
  134. */
  135. public $oldSalt;
  136. /**
  137. * setUp method
  138. *
  139. * Initializes environment state.
  140. *
  141. * @return void
  142. */
  143. public function setUp()
  144. {
  145. parent::setUp();
  146. $this->server = $_SERVER;
  147. $session = new Session();
  148. $request = new ServerRequest([
  149. 'url' => '/articles/index',
  150. 'session' => $session,
  151. 'params' => ['controller' => 'articles', 'action' => 'index'],
  152. ]);
  153. $this->Controller = new SecurityTestController($request);
  154. $this->Controller->Security = $this->Controller->TestSecurity;
  155. $this->Controller->Security->setConfig('blackHoleCallback', 'fail');
  156. $this->Security = $this->Controller->Security;
  157. $this->Security->session = $session;
  158. Security::setSalt('foo!');
  159. }
  160. /**
  161. * Resets environment state.
  162. *
  163. * @return void
  164. */
  165. public function tearDown()
  166. {
  167. parent::tearDown();
  168. $_SERVER = $this->server;
  169. $this->Security->session->delete('_Token');
  170. unset($this->Controller->Security);
  171. unset($this->Controller->Component);
  172. unset($this->Controller);
  173. }
  174. public function validatePost($expectedException = 'SecurityException', $expectedExceptionMessage = null)
  175. {
  176. try {
  177. return $this->Controller->Security->validatePost($this->Controller);
  178. } catch (SecurityException $ex) {
  179. $this->assertInstanceOf('Cake\\Controller\\Exception\\' . $expectedException, $ex);
  180. $this->assertEquals($expectedExceptionMessage, $ex->getMessage());
  181. return false;
  182. }
  183. }
  184. /**
  185. * testBlackholeWithBrokenCallback method
  186. *
  187. * Test that requests are still blackholed when controller has incorrect
  188. * visibility keyword in the blackhole callback.
  189. *
  190. * @return void
  191. * @triggers Controller.startup $Controller, $this->Controller
  192. */
  193. public function testBlackholeWithBrokenCallback()
  194. {
  195. $this->expectException(\Cake\Http\Exception\BadRequestException::class);
  196. $request = new ServerRequest([
  197. 'url' => 'posts/index',
  198. 'session' => $this->Security->session,
  199. 'params' => [
  200. 'controller' => 'posts',
  201. 'action' => 'index',
  202. ],
  203. ]);
  204. $Controller = new \TestApp\Controller\SomePagesController($request);
  205. $event = new Event('Controller.startup', $Controller);
  206. $Security = new SecurityComponent($Controller->components());
  207. $Security->setConfig('blackHoleCallback', '_fail');
  208. $Security->startup($event);
  209. $Security->blackHole($Controller, 'csrf');
  210. }
  211. /**
  212. * testExceptionWhenActionIsBlackholeCallback method
  213. *
  214. * Ensure that directly requesting the blackholeCallback as the controller
  215. * action results in an exception.
  216. *
  217. * @return void
  218. * @triggers Controller.startup $this->Controller
  219. */
  220. public function testExceptionWhenActionIsBlackholeCallback()
  221. {
  222. $this->Controller->request = $this->Controller->request
  223. ->withParam('controller', 'posts')
  224. ->withParam('action', 'fail');
  225. $event = new Event('Controller.startup', $this->Controller);
  226. $this->assertFalse($this->Controller->failed);
  227. $this->Controller->Security->startup($event);
  228. $this->assertTrue($this->Controller->failed, 'Request was blackholed.');
  229. }
  230. /**
  231. * test blackholeCallback returning a response
  232. *
  233. * @return void
  234. */
  235. public function testBlackholeReturnResponse()
  236. {
  237. $request = new ServerRequest([
  238. 'url' => 'posts/index',
  239. 'session' => $this->Security->session,
  240. 'method' => 'POST',
  241. 'params' => [
  242. 'controller' => 'posts',
  243. 'action' => 'index',
  244. ],
  245. 'post' => [
  246. 'key' => 'value',
  247. ],
  248. ]);
  249. $Controller = new \TestApp\Controller\SomePagesController($request);
  250. $event = new Event('Controller.startup', $Controller);
  251. $Security = new SecurityComponent($Controller->components());
  252. $Security->setConfig('blackHoleCallback', 'responseGenerator');
  253. $result = $Security->startup($event);
  254. $this->assertInstanceOf(Response::class, $result);
  255. }
  256. /**
  257. * testConstructorSettingProperties method
  258. *
  259. * Test that initialize can set properties.
  260. *
  261. * @return void
  262. */
  263. public function testConstructorSettingProperties()
  264. {
  265. $settings = [
  266. 'requireSecure' => ['update_account'],
  267. 'validatePost' => false,
  268. ];
  269. $Security = new SecurityComponent($this->Controller->components(), $settings);
  270. $this->assertEquals($Security->validatePost, $settings['validatePost']);
  271. }
  272. /**
  273. * testStartup method
  274. *
  275. * @return void
  276. * @triggers Controller.startup $this->Controller
  277. */
  278. public function testStartup()
  279. {
  280. $event = new Event('Controller.startup', $this->Controller);
  281. $this->Controller->Security->startup($event);
  282. $this->assertTrue($this->Security->session->check('_Token'));
  283. }
  284. /**
  285. * testRequireSecureFail method
  286. *
  287. * @return void
  288. * @triggers Controller.startup $this->Controller
  289. */
  290. public function testRequireSecureFail()
  291. {
  292. $this->Controller->request = $this->Controller->request
  293. ->withParam('action', 'posted')
  294. ->withEnv('HTTPS', 'off')
  295. ->withEnv('REQUEST_METHOD', 'POST');
  296. $event = new Event('Controller.startup', $this->Controller);
  297. $this->Controller->Security->requireSecure(['posted']);
  298. $this->Controller->Security->startup($event);
  299. $this->assertTrue($this->Controller->failed);
  300. }
  301. /**
  302. * testRequireSecureSucceed method
  303. *
  304. * @return void
  305. * @triggers Controller.startup $this->Controller
  306. */
  307. public function testRequireSecureSucceed()
  308. {
  309. $this->Controller->request = $this->Controller->request
  310. ->withParam('action', 'posted')
  311. ->withEnv('HTTPS', 'on')
  312. ->withEnv('REQUEST_METHOD', 'Secure');
  313. $event = new Event('Controller.startup', $this->Controller);
  314. $this->Controller->Security->requireSecure('posted');
  315. $this->Controller->Security->startup($event);
  316. $this->assertFalse($this->Controller->failed);
  317. }
  318. /**
  319. * testRequireSecureEmptyFail method
  320. *
  321. * @return void
  322. * @triggers Controller.startup $this->Controller
  323. */
  324. public function testRequireSecureEmptyFail()
  325. {
  326. $this->Controller->request = $this->Controller->request
  327. ->withParam('action', 'posted')
  328. ->withEnv('HTTPS', 'off')
  329. ->withEnv('REQUEST_METHOD', 'POST');
  330. $event = new Event('Controller.startup', $this->Controller);
  331. $this->Controller->Security->requireSecure();
  332. $this->Controller->Security->startup($event);
  333. $this->assertTrue($this->Controller->failed);
  334. }
  335. /**
  336. * testRequireSecureEmptySucceed method
  337. *
  338. * @return void
  339. * @triggers Controller.startup $this->Controller
  340. */
  341. public function testRequireSecureEmptySucceed()
  342. {
  343. $this->Controller->request = $this->Controller->request
  344. ->withParam('action', 'posted')
  345. ->withEnv('HTTPS', 'on')
  346. ->withEnv('REQUEST_METHOD', 'Secure');
  347. $event = new Event('Controller.startup', $this->Controller);
  348. $this->Controller->Security->requireSecure();
  349. $this->Controller->Security->startup($event);
  350. $this->assertFalse($this->Controller->failed);
  351. }
  352. /**
  353. * testRequireAuthFail method
  354. *
  355. * @group deprecated
  356. * @return void
  357. * @triggers Controller.startup $this->Controller
  358. */
  359. public function testRequireAuthFail()
  360. {
  361. $this->deprecated(function () {
  362. $event = new Event('Controller.startup', $this->Controller);
  363. $this->Controller->request = $this->Controller->request
  364. ->withParam('action', 'posted')
  365. ->withData('username', 'willy')
  366. ->withData('password', 'somePass')
  367. ->withEnv('REQUEST_METHOD', 'AUTH');
  368. $this->Security->requireAuth(['posted']);
  369. $this->Security->startup($event);
  370. $this->assertTrue($this->Controller->failed);
  371. $this->Controller->request->getSession()->write('_Token', ['allowedControllers' => []]);
  372. $this->Security->requireAuth('posted');
  373. $this->Security->startup($event);
  374. $this->assertTrue($this->Controller->failed);
  375. $this->Controller->request->getSession()->write('_Token', [
  376. 'allowedControllers' => ['SecurityTest'], 'allowedActions' => ['posted2'],
  377. ]);
  378. $this->Security->requireAuth('posted');
  379. $this->Security->startup($event);
  380. $this->assertTrue($this->Controller->failed);
  381. });
  382. }
  383. /**
  384. * testRequireAuthSucceed method
  385. *
  386. * @group deprecated
  387. * @return void
  388. * @triggers Controller.startup $this->Controller
  389. */
  390. public function testRequireAuthSucceed()
  391. {
  392. $this->deprecated(function () {
  393. $_SERVER['REQUEST_METHOD'] = 'AUTH';
  394. $this->Controller->Security->setConfig('validatePost', false);
  395. $event = new Event('Controller.startup', $this->Controller);
  396. $this->Controller->request->addParams([
  397. 'action' => 'posted',
  398. ]);
  399. $this->Security->requireAuth('posted');
  400. $this->Security->startup($event);
  401. $this->assertFalse($this->Controller->failed);
  402. $this->Controller->Security->session->write('_Token', [
  403. 'allowedControllers' => ['SecurityTest'],
  404. 'allowedActions' => ['posted'],
  405. ]);
  406. $this->Controller->request->addParams([
  407. 'controller' => 'SecurityTest',
  408. 'action' => 'posted',
  409. ]);
  410. $this->Controller->request->data = [
  411. 'username' => 'willy',
  412. 'password' => 'somePass',
  413. '_Token' => '',
  414. ];
  415. $this->Controller->action = 'posted';
  416. $this->Controller->Security->requireAuth('posted');
  417. $this->Controller->Security->startup($event);
  418. $this->assertFalse($this->Controller->failed);
  419. });
  420. }
  421. /**
  422. * testValidatePost method
  423. *
  424. * Simple hash validation test
  425. *
  426. * @return void
  427. * @triggers Controller.startup $this->Controller
  428. */
  429. public function testValidatePost()
  430. {
  431. $event = new Event('Controller.startup', $this->Controller);
  432. $this->Security->startup($event);
  433. $fields = '4697b45f7f430ff3ab73018c20f315eecb0ba5a6%3AModel.valid';
  434. $unlocked = '';
  435. $debug = '';
  436. $this->Controller->request = $this->Controller->request->withParsedBody([
  437. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  438. '_Token' => compact('fields', 'unlocked', 'debug'),
  439. ]);
  440. $this->assertTrue($this->validatePost());
  441. }
  442. /**
  443. * testValidatePostOnGetWithData method
  444. *
  445. * Test that validatePost fires on GET with request data.
  446. * This could happen when method overriding is used.
  447. *
  448. * @return void
  449. * @triggers Controller.startup $this->Controller
  450. */
  451. public function testValidatePostOnGetWithData()
  452. {
  453. $event = new Event('Controller.startup', $this->Controller);
  454. $this->Security->startup($event);
  455. $fields = 'an-invalid-token';
  456. $unlocked = '';
  457. $debug = urlencode(json_encode([
  458. 'some-action',
  459. [],
  460. [],
  461. ]));
  462. $this->Controller->request = $this->Controller->request
  463. ->withEnv('REQUEST_METHOD', 'GET')
  464. ->withData('Model', ['username' => 'nate', 'password' => 'foo', 'valid' => '0'])
  465. ->withData('_Token', compact('fields', 'unlocked', 'debug'));
  466. $this->Security->startup($event);
  467. $this->assertTrue($this->Controller->failed);
  468. }
  469. /**
  470. * testValidatePostNoSession method
  471. *
  472. * Test that validatePost fails if you are missing the session information.
  473. *
  474. * @return void
  475. * @triggers Controller.startup $this->Controller
  476. */
  477. public function testValidatePostNoSession()
  478. {
  479. $event = new Event('Controller.startup', $this->Controller);
  480. $this->Security->startup($event);
  481. $this->Security->session->delete('_Token');
  482. $unlocked = '';
  483. $debug = urlencode(json_encode([
  484. '/articles/index',
  485. [],
  486. [],
  487. ]));
  488. $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
  489. $this->Controller->request = $this->Controller->request->withParsedBody([
  490. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  491. '_Token' => compact('fields', 'unlocked', 'debug'),
  492. ]);
  493. $this->assertFalse($this->validatePost('AuthSecurityException', 'Unexpected field \'Model.password\' in POST data, Unexpected field \'Model.username\' in POST data'));
  494. }
  495. /**
  496. * testValidatePostNoUnlockedInRequestData method
  497. *
  498. * Test that validatePost fails if you are missing unlocked in request data.
  499. *
  500. * @return void
  501. * @triggers Controller.startup $this->Controller
  502. */
  503. public function testValidatePostNoUnlockedInRequestData()
  504. {
  505. $event = new Event('Controller.startup', $this->Controller);
  506. $this->Security->startup($event);
  507. $this->Security->session->delete('_Token');
  508. $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
  509. $this->Controller->request = $this->Controller->request->withParsedBody([
  510. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  511. '_Token' => compact('fields'),
  512. ]);
  513. $this->assertFalse($this->validatePost('AuthSecurityException', '\'_Token.unlocked\' was not found in request data.'));
  514. }
  515. /**
  516. * testValidatePostFormTampering method
  517. *
  518. * Test that validatePost fails if any of its required fields are missing.
  519. *
  520. * @return void
  521. * @triggers Controller.startup $this->Controller
  522. */
  523. public function testValidatePostFormTampering()
  524. {
  525. $event = new Event('Controller.startup', $this->Controller);
  526. $this->Security->startup($event);
  527. $unlocked = '';
  528. $this->Controller->request = $this->Controller->request->withParsedBody([
  529. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  530. '_Token' => compact('unlocked'),
  531. ]);
  532. $result = $this->validatePost('AuthSecurityException', '\'_Token.fields\' was not found in request data.');
  533. $this->assertFalse($result, 'validatePost passed when fields were missing. %s');
  534. }
  535. /**
  536. * testValidatePostEmptyForm method
  537. *
  538. * Test that validatePost fails if empty form is submitted.
  539. *
  540. * @return void
  541. * @triggers Controller.startup $this->Controller
  542. */
  543. public function testValidatePostEmptyForm()
  544. {
  545. $this->Controller->request = $this->Controller->request
  546. ->withEnv('REQUEST_METHOD', 'POST')
  547. ->withParsedBody([]);
  548. $event = new Event('Controller.startup', $this->Controller);
  549. $this->Security->startup($event);
  550. $result = $this->validatePost('AuthSecurityException', '\'_Token\' was not found in request data.');
  551. $this->assertFalse($result, 'validatePost passed when empty form is submitted');
  552. }
  553. /**
  554. * testValidatePostObjectDeserialize
  555. *
  556. * Test that objects can't be passed into the serialized string. This was a vector for RFI and LFI
  557. * attacks. Thanks to Felix Wilhelm
  558. *
  559. * @return void
  560. * @triggers Controller.startup $this->Controller
  561. */
  562. public function testValidatePostObjectDeserialize()
  563. {
  564. $event = new Event('Controller.startup', $this->Controller);
  565. $this->Security->startup($event);
  566. $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877';
  567. $unlocked = '';
  568. $debug = urlencode(json_encode([
  569. '/articles/index',
  570. ['Model.password', 'Model.username', 'Model.valid'],
  571. [],
  572. ]));
  573. // a corrupted serialized object, so we can see if it ever gets to deserialize
  574. $attack = 'O:3:"App":1:{s:5:"__map";a:1:{s:3:"foo";s:7:"Hacked!";s:1:"fail"}}';
  575. $fields .= urlencode(':' . str_rot13($attack));
  576. $this->Controller->request = $this->Controller->request->withParsedBody([
  577. 'Model' => ['username' => 'mark', 'password' => 'foo', 'valid' => '0'],
  578. '_Token' => compact('fields', 'unlocked', 'debug'),
  579. ]);
  580. $result = $this->validatePost('SecurityException', 'Bad Request');
  581. $this->assertFalse($result, 'validatePost passed when key was missing. %s');
  582. }
  583. /**
  584. * testValidatePostIgnoresCsrfToken method
  585. *
  586. * Tests validation post data ignores `_csrfToken`.
  587. *
  588. * @return void
  589. * @triggers Controller.startup $this->Controller
  590. */
  591. public function testValidatePostIgnoresCsrfToken()
  592. {
  593. $event = new Event('Controller.startup', $this->Controller);
  594. $this->Security->startup($event);
  595. $fields = 'f95b472a63f1d883b9eaacaf8a8e36e325e3fe82%3A';
  596. $unlocked = '';
  597. $debug = 'not used';
  598. $this->Controller->request = $this->Controller->request->withParsedBody([
  599. '_csrfToken' => 'abc123',
  600. 'Model' => ['multi_field' => ['1', '3']],
  601. '_Token' => compact('fields', 'unlocked', 'debug'),
  602. ]);
  603. $this->assertTrue($this->validatePost());
  604. }
  605. /**
  606. * testValidatePostArray method
  607. *
  608. * Tests validation of checkbox arrays.
  609. *
  610. * @return void
  611. * @triggers Controller.startup $this->Controller
  612. */
  613. public function testValidatePostArray()
  614. {
  615. $event = new Event('Controller.startup', $this->Controller);
  616. $this->Security->startup($event);
  617. $fields = 'f95b472a63f1d883b9eaacaf8a8e36e325e3fe82%3A';
  618. $unlocked = '';
  619. $debug = urlencode(json_encode([
  620. 'some-action',
  621. [],
  622. [],
  623. ]));
  624. $this->Controller->request = $this->Controller->request->withParsedBody([
  625. 'Model' => ['multi_field' => ['1', '3']],
  626. '_Token' => compact('fields', 'unlocked', 'debug'),
  627. ]);
  628. $this->assertTrue($this->validatePost());
  629. $this->Controller->request = $this->Controller->request->withParsedBody([
  630. 'Model' => ['multi_field' => [12 => '1', 20 => '3']],
  631. '_Token' => compact('fields', 'unlocked', 'debug'),
  632. ]);
  633. $this->assertTrue($this->validatePost());
  634. }
  635. /**
  636. * testValidateIntFieldName method
  637. *
  638. * Tests validation of integer field names.
  639. *
  640. * @return void
  641. */
  642. public function testValidateIntFieldName()
  643. {
  644. $event = new Event('Controller.startup', $this->Controller);
  645. $this->Security->startup($event);
  646. $fields = '11f87a5962db9ac26405e460cd3063bb6ff76cf8%3A';
  647. $unlocked = '';
  648. $debug = urlencode(json_encode([
  649. 'some-action',
  650. [],
  651. [],
  652. ]));
  653. $this->Controller->request = $this->Controller->request->withParsedBody([
  654. 1 => 'value,',
  655. '_Token' => compact('fields', 'unlocked', 'debug'),
  656. ]);
  657. $this->assertTrue($this->validatePost());
  658. }
  659. /**
  660. * testValidatePostNoModel method
  661. *
  662. * @return void
  663. * @triggers Controller.startup $this->Controller
  664. */
  665. public function testValidatePostNoModel()
  666. {
  667. $event = new Event('Controller.startup', $this->Controller);
  668. $this->Security->startup($event);
  669. $fields = 'a2a942f587deb20e90241c51b59d901d8a7f796b%3A';
  670. $unlocked = '';
  671. $debug = 'not used';
  672. $this->Controller->request = $this->Controller->request->withParsedBody([
  673. 'anything' => 'some_data',
  674. '_Token' => compact('fields', 'unlocked', 'debug'),
  675. ]);
  676. $result = $this->validatePost();
  677. $this->assertTrue($result);
  678. }
  679. /**
  680. * testValidatePostSimple method
  681. *
  682. * @return void
  683. * @triggers Controller.startup $this->Controller
  684. */
  685. public function testValidatePostSimple()
  686. {
  687. $event = new Event('Controller.startup', $this->Controller);
  688. $this->Security->startup($event);
  689. $fields = 'de2ca3670dd06c29558dd98482c8739e86da2c7c%3A';
  690. $unlocked = '';
  691. $debug = 'not used';
  692. $this->Controller->request = $this->Controller->request->withParsedBody([
  693. 'Model' => ['username' => '', 'password' => ''],
  694. '_Token' => compact('fields', 'unlocked', 'debug'),
  695. ]);
  696. $result = $this->validatePost();
  697. $this->assertTrue($result);
  698. }
  699. /**
  700. * test validatePost uses full URL
  701. *
  702. * @return void
  703. * @triggers Controller.startup $this->Controller
  704. */
  705. public function testValidatePostSubdirectory()
  706. {
  707. // set the base path.
  708. $this->Controller->request = $this->Controller->request
  709. ->withAttribute('base', 'subdir')
  710. ->withAttributE('webroot', 'subdir/');
  711. Router::pushRequest($this->Controller->request);
  712. $event = new Event('Controller.startup', $this->Controller);
  713. $this->Security->startup($event);
  714. // Differs from testValidatePostSimple because of base url
  715. $fields = 'cc9b6af3f33147235ae8f8037b0a71399a2425f2%3A';
  716. $unlocked = '';
  717. $debug = '';
  718. $this->Controller->request = $this->Controller->request->withParsedBody([
  719. 'Model' => ['username' => '', 'password' => ''],
  720. '_Token' => compact('fields', 'unlocked', 'debug'),
  721. ]);
  722. $result = $this->validatePost();
  723. $this->assertTrue($result);
  724. }
  725. /**
  726. * testValidatePostComplex method
  727. *
  728. * Tests hash validation for multiple records, including locked fields.
  729. *
  730. * @return void
  731. * @triggers Controller.startup $this->Controller
  732. */
  733. public function testValidatePostComplex()
  734. {
  735. $event = new Event('Controller.startup', $this->Controller);
  736. $this->Security->startup($event);
  737. $fields = 'b00b7e5c2e3bf8bc474fb7cfde6f9c2aa06ab9bc%3AAddresses.0.id%7CAddresses.1.id';
  738. $unlocked = '';
  739. $debug = 'not used';
  740. $this->Controller->request = $this->Controller->request->withParsedBody([
  741. 'Addresses' => [
  742. '0' => [
  743. 'id' => '123456', 'title' => '', 'first_name' => '', 'last_name' => '',
  744. 'address' => '', 'city' => '', 'phone' => '', 'primary' => '',
  745. ],
  746. '1' => [
  747. 'id' => '654321', 'title' => '', 'first_name' => '', 'last_name' => '',
  748. 'address' => '', 'city' => '', 'phone' => '', 'primary' => '',
  749. ],
  750. ],
  751. '_Token' => compact('fields', 'unlocked', 'debug'),
  752. ]);
  753. $result = $this->validatePost();
  754. $this->assertTrue($result);
  755. }
  756. /**
  757. * testValidatePostMultipleSelect method
  758. *
  759. * Test ValidatePost with multiple select elements.
  760. *
  761. * @return void
  762. * @triggers Controller.startup $this->Controller
  763. */
  764. public function testValidatePostMultipleSelect()
  765. {
  766. $event = new Event('Controller.startup', $this->Controller);
  767. $this->Security->startup($event);
  768. $fields = '28dd05f0af314050784b18b3366857e8e8c78e73%3A';
  769. $unlocked = '';
  770. $debug = 'not used';
  771. $this->Controller->request = $this->Controller->request->withParsedBody([
  772. 'Tag' => ['Tag' => [1, 2]],
  773. '_Token' => compact('fields', 'unlocked', 'debug'),
  774. ]);
  775. $result = $this->validatePost();
  776. $this->assertTrue($result);
  777. $this->Controller->request = $this->Controller->request->withParsedBody([
  778. 'Tag' => ['Tag' => [1, 2, 3]],
  779. '_Token' => compact('fields', 'unlocked', 'debug'),
  780. ]);
  781. $result = $this->validatePost();
  782. $this->assertTrue($result);
  783. $this->Controller->request = $this->Controller->request->withParsedBody([
  784. 'Tag' => ['Tag' => [1, 2, 3, 4]],
  785. '_Token' => compact('fields', 'unlocked', 'debug'),
  786. ]);
  787. $result = $this->validatePost();
  788. $this->assertTrue($result);
  789. $fields = '1e4c9269b64756e9b141d364497c5f037b428a37%3A';
  790. $this->Controller->request = $this->Controller->request->withParsedBody([
  791. 'User.password' => 'bar', 'User.name' => 'foo', 'User.is_valid' => '1',
  792. 'Tag' => ['Tag' => [1]],
  793. '_Token' => compact('fields', 'unlocked', 'debug'),
  794. ]);
  795. $result = $this->validatePost();
  796. $this->assertTrue($result);
  797. }
  798. /**
  799. * testValidatePostCheckbox method
  800. *
  801. * First block tests un-checked checkbox
  802. * Second block tests checked checkbox
  803. *
  804. * @return void
  805. * @triggers Controller.startup $this->Controller
  806. */
  807. public function testValidatePostCheckbox()
  808. {
  809. $event = new Event('Controller.startup', $this->Controller);
  810. $this->Security->startup($event);
  811. $fields = '4697b45f7f430ff3ab73018c20f315eecb0ba5a6%3AModel.valid';
  812. $unlocked = '';
  813. $debug = 'not used';
  814. $this->Controller->request = $this->Controller->request->withParsedBody([
  815. 'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
  816. '_Token' => compact('fields', 'unlocked', 'debug'),
  817. ]);
  818. $result = $this->validatePost();
  819. $this->assertTrue($result);
  820. $fields = '3f368401f9a8610bcace7746039651066cdcdc38%3A';
  821. $this->Controller->request = $this->Controller->request->withParsedBody([
  822. 'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
  823. '_Token' => compact('fields', 'unlocked', 'debug'),
  824. ]);
  825. $result = $this->validatePost();
  826. $this->assertTrue($result);
  827. $this->Controller->request = $this->Controller->request->withParsedBody([]);
  828. $this->Security->startup($event);
  829. $this->Controller->request = $this->Controller->request->withParsedBody([
  830. 'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
  831. '_Token' => compact('fields', 'unlocked', 'debug'),
  832. ]);
  833. $result = $this->validatePost();
  834. $this->assertTrue($result);
  835. }
  836. /**
  837. * testValidatePostHidden method
  838. *
  839. * @return void
  840. * @triggers Controller.startup $this->Controller
  841. */
  842. public function testValidatePostHidden()
  843. {
  844. $event = new Event('Controller.startup', $this->Controller);
  845. $this->Security->startup($event);
  846. $fields = '96e61bded2b62b0c420116a0eb06a3b3acddb8f1%3AModel.hidden%7CModel.other_hidden';
  847. $unlocked = '';
  848. $debug = 'not used';
  849. $this->Controller->request = $this->Controller->request->withParsedBody([
  850. 'Model' => [
  851. 'username' => '', 'password' => '', 'hidden' => '0',
  852. 'other_hidden' => 'some hidden value',
  853. ],
  854. '_Token' => compact('fields', 'unlocked', 'debug'),
  855. ]);
  856. $result = $this->validatePost();
  857. $this->assertTrue($result);
  858. }
  859. /**
  860. * testValidatePostWithDisabledFields method
  861. *
  862. * @return void
  863. * @triggers Controller.startup $this->Controller
  864. */
  865. public function testValidatePostWithDisabledFields()
  866. {
  867. $event = new Event('Controller.startup', $this->Controller);
  868. $this->Security->setConfig('disabledFields', ['Model.username', 'Model.password']);
  869. $this->Security->startup($event);
  870. $fields = '0313460e1136e1227044399fe10a6edc0cbca279%3AModel.hidden';
  871. $unlocked = '';
  872. $debug = 'not used';
  873. $this->Controller->request = $this->Controller->request->withParsedBody([
  874. 'Model' => [
  875. 'username' => '', 'password' => '', 'hidden' => '0',
  876. ],
  877. '_Token' => compact('fields', 'unlocked', 'debug'),
  878. ]);
  879. $result = $this->validatePost();
  880. $this->assertTrue($result);
  881. }
  882. /**
  883. * testValidatePostDisabledFieldsInData method
  884. *
  885. * Test validating post data with posted unlocked fields.
  886. *
  887. * @return void
  888. * @triggers Controller.startup $this->Controller
  889. */
  890. public function testValidatePostDisabledFieldsInData()
  891. {
  892. $event = new Event('Controller.startup', $this->Controller);
  893. $this->Security->startup($event);
  894. $unlocked = 'Model.username';
  895. $fields = ['Model.hidden', 'Model.password'];
  896. $fields = urlencode(
  897. hash_hmac('sha1', '/articles/index' . serialize($fields) . $unlocked . 'cli', Security::getSalt())
  898. );
  899. $debug = 'not used';
  900. $this->Controller->request = $this->Controller->request->withParsedBody([
  901. 'Model' => [
  902. 'username' => 'mark',
  903. 'password' => 'sekret',
  904. 'hidden' => '0',
  905. ],
  906. '_Token' => compact('fields', 'unlocked', 'debug'),
  907. ]);
  908. $result = $this->validatePost();
  909. $this->assertTrue($result);
  910. }
  911. /**
  912. * testValidatePostFailNoDisabled method
  913. *
  914. * Test that missing 'unlocked' input causes failure.
  915. *
  916. * @return void
  917. * @triggers Controller.startup $this->Controller
  918. */
  919. public function testValidatePostFailNoDisabled()
  920. {
  921. $event = new Event('Controller.startup', $this->Controller);
  922. $this->Security->startup($event);
  923. $fields = ['Model.hidden', 'Model.password', 'Model.username'];
  924. $fields = urlencode(Security::hash(serialize($fields) . Security::getSalt()));
  925. $this->Controller->request = $this->Controller->request->withParsedBody([
  926. 'Model' => [
  927. 'username' => 'mark',
  928. 'password' => 'sekret',
  929. 'hidden' => '0',
  930. ],
  931. '_Token' => compact('fields'),
  932. ]);
  933. $result = $this->validatePost('SecurityException', '\'_Token.unlocked\' was not found in request data.');
  934. $this->assertFalse($result);
  935. }
  936. /**
  937. * testValidatePostFailNoDebug method
  938. *
  939. * Test that missing 'debug' input causes failure.
  940. *
  941. * @return void
  942. * @triggers Controller.startup $this->Controller
  943. */
  944. public function testValidatePostFailNoDebug()
  945. {
  946. $event = new Event('Controller.startup', $this->Controller);
  947. $this->Security->startup($event);
  948. $fields = ['Model.hidden', 'Model.password', 'Model.username'];
  949. $fields = urlencode(Security::hash(serialize($fields) . Security::getSalt()));
  950. $unlocked = '';
  951. $this->Controller->request = $this->Controller->request->withParsedBody([
  952. 'Model' => [
  953. 'username' => 'mark',
  954. 'password' => 'sekret',
  955. 'hidden' => '0',
  956. ],
  957. '_Token' => compact('fields', 'unlocked'),
  958. ]);
  959. $result = $this->validatePost('SecurityException', '\'_Token.debug\' was not found in request data.');
  960. $this->assertFalse($result);
  961. }
  962. /**
  963. * testValidatePostFailNoDebugMode method
  964. *
  965. * Test that missing 'debug' input is not the problem when debug mode disabled.
  966. *
  967. * @return void
  968. * @triggers Controller.startup $this->Controller
  969. */
  970. public function testValidatePostFailNoDebugMode()
  971. {
  972. $event = new Event('Controller.startup', $this->Controller);
  973. $this->Security->startup($event);
  974. $fields = ['Model.hidden', 'Model.password', 'Model.username'];
  975. $fields = urlencode(Security::hash(serialize($fields) . Security::getSalt()));
  976. $unlocked = '';
  977. $this->Controller->request = $this->Controller->request->withParsedBody([
  978. 'Model' => [
  979. 'username' => 'mark',
  980. 'password' => 'sekret',
  981. 'hidden' => '0',
  982. ],
  983. '_Token' => compact('fields', 'unlocked'),
  984. ]);
  985. Configure::write('debug', false);
  986. $result = $this->validatePost('SecurityException', 'The request has been black-holed');
  987. }
  988. /**
  989. * testValidatePostFailDisabledFieldTampering method
  990. *
  991. * Test that validatePost fails when unlocked fields are changed.
  992. *
  993. * @return void
  994. * @triggers Controller.startup $this->Controller
  995. */
  996. public function testValidatePostFailDisabledFieldTampering()
  997. {
  998. $event = new Event('Controller.startup', $this->Controller);
  999. $this->Security->startup($event);
  1000. $unlocked = 'Model.username';
  1001. $fields = ['Model.hidden', 'Model.password'];
  1002. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  1003. $debug = urlencode(json_encode([
  1004. '/articles/index',
  1005. ['Model.hidden', 'Model.password'],
  1006. ['Model.username'],
  1007. ]));
  1008. // Tamper the values.
  1009. $unlocked = 'Model.username|Model.password';
  1010. $this->Controller->request = $this->Controller->request->withParsedBody([
  1011. 'Model' => [
  1012. 'username' => 'mark',
  1013. 'password' => 'sekret',
  1014. 'hidden' => '0',
  1015. ],
  1016. '_Token' => compact('fields', 'unlocked', 'debug'),
  1017. ]);
  1018. $result = $this->validatePost('SecurityException', 'Missing field \'Model.password\' in POST data, Unexpected unlocked field \'Model.password\' in POST data');
  1019. $this->assertFalse($result);
  1020. }
  1021. /**
  1022. * Test that invalid types cause failures.
  1023. *
  1024. * @return void
  1025. */
  1026. public function testValidatePostFailArrayData()
  1027. {
  1028. $event = new Event('Controller.startup', $this->Controller);
  1029. $this->Security->startup($event);
  1030. $this->Controller->request = $this->Controller->request->withParsedBody([
  1031. 'Model' => [
  1032. 'username' => 'mark',
  1033. 'password' => 'sekret',
  1034. ],
  1035. '_Token' => [
  1036. 'fields' => [],
  1037. 'unlocked' => '',
  1038. ],
  1039. ]);
  1040. Configure::write('debug', false);
  1041. $result = $this->validatePost('SecurityException', "'_Token.fields' was invalid.");
  1042. $this->assertFalse($result);
  1043. }
  1044. /**
  1045. * testValidateHiddenMultipleModel method
  1046. *
  1047. * @return void
  1048. * @triggers Controller.startup $this->Controller
  1049. */
  1050. public function testValidateHiddenMultipleModel()
  1051. {
  1052. $event = new Event('Controller.startup', $this->Controller);
  1053. $this->Security->startup($event);
  1054. $fields = '642b7a6db3b848fab88952b86ea36c572f93df40%3AModel.valid%7CModel2.valid%7CModel3.valid';
  1055. $unlocked = '';
  1056. $debug = 'not used';
  1057. $this->Controller->request = $this->Controller->request->withParsedBody([
  1058. 'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
  1059. 'Model2' => ['valid' => '0'],
  1060. 'Model3' => ['valid' => '0'],
  1061. '_Token' => compact('fields', 'unlocked', 'debug'),
  1062. ]);
  1063. $result = $this->validatePost();
  1064. $this->assertTrue($result);
  1065. }
  1066. /**
  1067. * testValidateHasManyModel method
  1068. *
  1069. * @return void
  1070. * @triggers Controller.startup $this->Controller
  1071. */
  1072. public function testValidateHasManyModel()
  1073. {
  1074. $event = new Event('Controller.startup', $this->Controller);
  1075. $this->Security->startup($event);
  1076. $fields = '792324c8a374772ad82acfb28f0e77e70f8ed3af%3AModel.0.hidden%7CModel.0.valid';
  1077. $fields .= '%7CModel.1.hidden%7CModel.1.valid';
  1078. $unlocked = '';
  1079. $debug = 'not used';
  1080. $this->Controller->request = $this->Controller->request->withParsedBody([
  1081. 'Model' => [
  1082. [
  1083. 'username' => 'username', 'password' => 'password',
  1084. 'hidden' => 'value', 'valid' => '0',
  1085. ],
  1086. [
  1087. 'username' => 'username', 'password' => 'password',
  1088. 'hidden' => 'value', 'valid' => '0',
  1089. ],
  1090. ],
  1091. '_Token' => compact('fields', 'unlocked', 'debug'),
  1092. ]);
  1093. $result = $this->validatePost();
  1094. $this->assertTrue($result);
  1095. }
  1096. /**
  1097. * testValidateHasManyRecordsPass method
  1098. *
  1099. * @return void
  1100. * @triggers Controller.startup $this->Controller
  1101. */
  1102. public function testValidateHasManyRecordsPass()
  1103. {
  1104. $event = new Event('Controller.startup', $this->Controller);
  1105. $this->Security->startup($event);
  1106. $fields = '7f4bff67558e25ebeea44c84ea4befa8d50b080c%3AAddress.0.id%7CAddress.0.primary%7C';
  1107. $fields .= 'Address.1.id%7CAddress.1.primary';
  1108. $unlocked = '';
  1109. $debug = 'not used';
  1110. $this->Controller->request = $this->Controller->request->withParsedBody([
  1111. 'Address' => [
  1112. 0 => [
  1113. 'id' => '123',
  1114. 'title' => 'home',
  1115. 'first_name' => 'Bilbo',
  1116. 'last_name' => 'Baggins',
  1117. 'address' => '23 Bag end way',
  1118. 'city' => 'the shire',
  1119. 'phone' => 'N/A',
  1120. 'primary' => '1',
  1121. ],
  1122. 1 => [
  1123. 'id' => '124',
  1124. 'title' => 'home',
  1125. 'first_name' => 'Frodo',
  1126. 'last_name' => 'Baggins',
  1127. 'address' => '50 Bag end way',
  1128. 'city' => 'the shire',
  1129. 'phone' => 'N/A',
  1130. 'primary' => '1',
  1131. ],
  1132. ],
  1133. '_Token' => compact('fields', 'unlocked', 'debug'),
  1134. ]);
  1135. $result = $this->validatePost();
  1136. $this->assertTrue($result);
  1137. }
  1138. /**
  1139. * testValidateNestedNumericSets method
  1140. *
  1141. * Test that values like Foo.0.1
  1142. *
  1143. * @return void
  1144. * @triggers Controller.startup $this->Controller
  1145. */
  1146. public function testValidateNestedNumericSets()
  1147. {
  1148. $event = new Event('Controller.startup', $this->Controller);
  1149. $this->Security->startup($event);
  1150. $unlocked = '';
  1151. $hashFields = ['TaxonomyData'];
  1152. $fields = urlencode(
  1153. hash_hmac('sha1', '/articles/index' . serialize($hashFields) . $unlocked . 'cli', Security::getSalt())
  1154. );
  1155. $debug = 'not used';
  1156. $this->Controller->request = $this->Controller->request->withParsedBody([
  1157. 'TaxonomyData' => [
  1158. 1 => [[2]],
  1159. 2 => [[3]],
  1160. ],
  1161. '_Token' => compact('fields', 'unlocked', 'debug'),
  1162. ]);
  1163. $result = $this->validatePost();
  1164. $this->assertTrue($result);
  1165. }
  1166. /**
  1167. * testValidateHasManyRecords method
  1168. *
  1169. * validatePost should fail, hidden fields have been changed.
  1170. *
  1171. * @return void
  1172. * @triggers Controller.startup $this->Controller
  1173. */
  1174. public function testValidateHasManyRecordsFail()
  1175. {
  1176. $event = new Event('Controller.startup', $this->Controller);
  1177. $this->Security->startup($event);
  1178. $fields = '7a203edb3d345bbf38fe0dccae960da8842e11d7%3AAddress.0.id%7CAddress.0.primary%7C';
  1179. $fields .= 'Address.1.id%7CAddress.1.primary';
  1180. $unlocked = '';
  1181. $debug = urlencode(json_encode([
  1182. '/articles/index',
  1183. [
  1184. 'Address.0.address',
  1185. 'Address.0.city',
  1186. 'Address.0.first_name',
  1187. 'Address.0.last_name',
  1188. 'Address.0.phone',
  1189. 'Address.0.title',
  1190. 'Address.1.address',
  1191. 'Address.1.city',
  1192. 'Address.1.first_name',
  1193. 'Address.1.last_name',
  1194. 'Address.1.phone',
  1195. 'Address.1.title',
  1196. 'Address.0.id' => '123',
  1197. 'Address.0.primary' => '5',
  1198. 'Address.1.id' => '124',
  1199. 'Address.1.primary' => '1',
  1200. ],
  1201. [],
  1202. ]));
  1203. $this->Controller->request = $this->Controller->request->withParsedBody([
  1204. 'Address' => [
  1205. 0 => [
  1206. 'id' => '123',
  1207. 'title' => 'home',
  1208. 'first_name' => 'Bilbo',
  1209. 'last_name' => 'Baggins',
  1210. 'address' => '23 Bag end way',
  1211. 'city' => 'the shire',
  1212. 'phone' => 'N/A',
  1213. 'primary' => '5',
  1214. ],
  1215. 1 => [
  1216. 'id' => '124',
  1217. 'title' => 'home',
  1218. 'first_name' => 'Frodo',
  1219. 'last_name' => 'Baggins',
  1220. 'address' => '50 Bag end way',
  1221. 'city' => 'the shire',
  1222. 'phone' => 'N/A',
  1223. 'primary' => '1',
  1224. ],
  1225. ],
  1226. '_Token' => compact('fields', 'unlocked', 'debug'),
  1227. ]);
  1228. $result = $this->validatePost('SecurityException', 'Bad Request');
  1229. $this->assertFalse($result);
  1230. }
  1231. /**
  1232. * testFormDisabledFields method
  1233. *
  1234. * @return void
  1235. * @triggers Controller.startup $this->Controller
  1236. */
  1237. public function testFormDisabledFields()
  1238. {
  1239. $event = new Event('Controller.startup', $this->Controller);
  1240. $this->Security->startup($event);
  1241. $fields = '4eaf5c6b93d5140c24171d5fdce16ed5b904f288%3An%3A0%3A%7B%7D';
  1242. $unlocked = '';
  1243. $debug = urlencode(json_encode([
  1244. '/articles/index',
  1245. [],
  1246. [],
  1247. ]));
  1248. $this->Controller->request = $this->Controller->request->withParsedBody([
  1249. 'MyModel' => ['name' => 'some data'],
  1250. '_Token' => compact('fields', 'unlocked', 'debug'),
  1251. ]);
  1252. $result = $this->validatePost('SecurityException', 'Unexpected field \'MyModel.name\' in POST data');
  1253. $this->assertFalse($result);
  1254. $this->Security->startup($event);
  1255. $this->Security->setConfig('disabledFields', ['MyModel.name']);
  1256. $this->Controller->request = $this->Controller->request->withParsedBody([
  1257. 'MyModel' => ['name' => 'some data'],
  1258. '_Token' => compact('fields', 'unlocked', 'debug'),
  1259. ]);
  1260. $result = $this->validatePost();
  1261. $this->assertTrue($result);
  1262. }
  1263. /**
  1264. * testValidatePostRadio method
  1265. *
  1266. * Test validatePost with radio buttons.
  1267. *
  1268. * @return void
  1269. * @triggers Controller.startup $this->Controller
  1270. */
  1271. public function testValidatePostRadio()
  1272. {
  1273. $event = new Event('Controller.startup', $this->Controller);
  1274. $this->Security->startup($event);
  1275. $fields = 'a709dfdee0a0cce52c4c964a1b8a56159bb081b4%3An%3A0%3A%7B%7D';
  1276. $unlocked = '';
  1277. $debug = urlencode(json_encode([
  1278. '/articles/index',
  1279. [],
  1280. [],
  1281. ]));
  1282. $this->Controller->request = $this->Controller->request->withParsedBody([
  1283. '_Token' => compact('fields', 'unlocked', 'debug'),
  1284. ]);
  1285. $result = $this->validatePost('SecurityException', 'Bad Request');
  1286. $this->assertFalse($result);
  1287. $this->Controller->request = $this->Controller->request->withParsedBody([
  1288. '_Token' => compact('fields', 'unlocked', 'debug'),
  1289. 'Test' => ['test' => ''],
  1290. ]);
  1291. $result = $this->validatePost();
  1292. $this->assertTrue($result);
  1293. $this->Controller->request = $this->Controller->request->withParsedBody([
  1294. '_Token' => compact('fields', 'unlocked', 'debug'),
  1295. 'Test' => ['test' => '1'],
  1296. ]);
  1297. $result = $this->validatePost();
  1298. $this->assertTrue($result);
  1299. $this->Controller->request = $this->Controller->request->withParsedBody([
  1300. '_Token' => compact('fields', 'unlocked', 'debug'),
  1301. 'Test' => ['test' => '2'],
  1302. ]);
  1303. $result = $this->validatePost();
  1304. $this->assertTrue($result);
  1305. }
  1306. /**
  1307. * testValidatePostUrlAsHashInput method
  1308. *
  1309. * Test validatePost uses here() as a hash input.
  1310. *
  1311. * @return void
  1312. * @triggers Controller.startup $this->Controller
  1313. */
  1314. public function testValidatePostUrlAsHashInput()
  1315. {
  1316. $event = new Event('Controller.startup', $this->Controller);
  1317. $this->Security->startup($event);
  1318. $fields = 'de2ca3670dd06c29558dd98482c8739e86da2c7c%3A';
  1319. $unlocked = '';
  1320. $debug = urlencode(json_encode([
  1321. 'another-url',
  1322. ['Model.username', 'Model.password'],
  1323. [],
  1324. ]));
  1325. $this->Controller->request = $this->Controller->request
  1326. ->withData('Model', ['username' => '', 'password' => ''])
  1327. ->withData('_Token', compact('fields', 'unlocked', 'debug'));
  1328. $this->assertTrue($this->validatePost());
  1329. $this->Controller->request = $this->Controller->request
  1330. ->withRequestTarget('/posts/index?page=1');
  1331. $this->assertFalse($this->validatePost(
  1332. 'SecurityException',
  1333. 'URL mismatch in POST data (expected \'another-url\' but found \'/posts/index?page=1\')'
  1334. ));
  1335. $this->Controller->request = $this->Controller->request
  1336. ->withRequestTarget('/posts/edit/1');
  1337. $this->assertFalse($this->validatePost(
  1338. 'SecurityException',
  1339. 'URL mismatch in POST data (expected \'another-url\' but found \'/posts/edit/1\')'
  1340. ));
  1341. }
  1342. /**
  1343. * testBlackHoleNotDeletingSessionInformation method
  1344. *
  1345. * Test that blackhole doesn't delete the _Token session key so repeat data submissions
  1346. * stay blackholed.
  1347. *
  1348. * @return void
  1349. * @triggers Controller.startup $this->Controller
  1350. */
  1351. public function testBlackHoleNotDeletingSessionInformation()
  1352. {
  1353. $event = new Event('Controller.startup', $this->Controller);
  1354. $this->Security->startup($event);
  1355. $this->Security->blackHole($this->Controller, 'auth');
  1356. $this->assertTrue($this->Controller->Security->session->check('_Token'), '_Token was deleted by blackHole %s');
  1357. }
  1358. /**
  1359. * testGenerateToken method
  1360. *
  1361. * Test generateToken().
  1362. *
  1363. * @return void
  1364. */
  1365. public function testGenerateToken()
  1366. {
  1367. $request = $this->Controller->request;
  1368. $request = $this->Security->generateToken($request);
  1369. $this->assertNotEmpty($request->getParam('_Token'));
  1370. $this->assertSame([], $request->getParam('_Token.unlockedFields'));
  1371. }
  1372. /**
  1373. * testUnlockedActions method
  1374. *
  1375. * Test unlocked actions.
  1376. *
  1377. * @return void
  1378. * @triggers Controller.startup $this->Controller
  1379. */
  1380. public function testUnlockedActions()
  1381. {
  1382. $_SERVER['REQUEST_METHOD'] = 'POST';
  1383. $event = new Event('Controller.startup', $this->Controller);
  1384. $this->Controller->request = $this->Controller->request->withParsedBody(['data']);
  1385. $this->Security->unlockedActions = 'index';
  1386. $this->Security->blackHoleCallback = null;
  1387. $result = $this->Controller->Security->startup($event);
  1388. $this->assertNull($result);
  1389. }
  1390. /**
  1391. * testValidatePostDebugFormat method
  1392. *
  1393. * Test that debug token format is right.
  1394. *
  1395. * @return void
  1396. * @triggers Controller.startup $this->Controller
  1397. */
  1398. public function testValidatePostDebugFormat()
  1399. {
  1400. $event = new Event('Controller.startup', $this->Controller);
  1401. $this->Security->startup($event);
  1402. $unlocked = 'Model.username';
  1403. $fields = ['Model.hidden', 'Model.password'];
  1404. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  1405. $debug = urlencode(json_encode([
  1406. '/articles/index',
  1407. ['Model.hidden', 'Model.password'],
  1408. ['Model.username'],
  1409. ['not expected'],
  1410. ]));
  1411. $this->Controller->request = $this->Controller->request->withParsedBody([
  1412. 'Model' => [
  1413. 'username' => 'mark',
  1414. 'password' => 'sekret',
  1415. 'hidden' => '0',
  1416. ],
  1417. '_Token' => compact('fields', 'unlocked', 'debug'),
  1418. ]);
  1419. $result = $this->validatePost('SecurityException', 'Invalid security debug token.');
  1420. $this->assertFalse($result);
  1421. $debug = urlencode(json_encode('not an array'));
  1422. $result = $this->validatePost('SecurityException', 'Invalid security debug token.');
  1423. $this->assertFalse($result);
  1424. }
  1425. /**
  1426. * testBlackholeThrowsException method
  1427. *
  1428. * Test blackhole will now throw passed exception if debug enabled.
  1429. *
  1430. * @return void
  1431. */
  1432. public function testBlackholeThrowsException()
  1433. {
  1434. $this->expectException(\Cake\Controller\Exception\SecurityException::class);
  1435. $this->expectExceptionMessage('error description');
  1436. $this->Security->setConfig('blackHoleCallback', '');
  1437. $this->Security->blackHole($this->Controller, 'auth', new SecurityException('error description'));
  1438. }
  1439. /**
  1440. * testBlackholeThrowsBadRequest method
  1441. *
  1442. * Test blackhole will throw BadRequest if debug disabled.
  1443. *
  1444. * @return void
  1445. */
  1446. public function testBlackholeThrowsBadRequest()
  1447. {
  1448. $this->Security->setConfig('blackHoleCallback', '');
  1449. $message = '';
  1450. Configure::write('debug', false);
  1451. try {
  1452. $this->Security->blackHole($this->Controller, 'auth', new SecurityException('error description'));
  1453. } catch (SecurityException $ex) {
  1454. $message = $ex->getMessage();
  1455. $reason = $ex->getReason();
  1456. }
  1457. $this->assertEquals('The request has been black-holed', $message);
  1458. $this->assertEquals('error description', $reason);
  1459. }
  1460. /**
  1461. * testValidatePostFailTampering method
  1462. *
  1463. * Test that validatePost fails with tampered fields and explanation.
  1464. *
  1465. * @return void
  1466. * @triggers Controller.startup $this->Controller
  1467. */
  1468. public function testValidatePostFailTampering()
  1469. {
  1470. $event = new Event('Controller.startup', $this->Controller);
  1471. $this->Security->startup($event);
  1472. $unlocked = '';
  1473. $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
  1474. $debug = urlencode(json_encode([
  1475. '/articles/index',
  1476. $fields,
  1477. [],
  1478. ]));
  1479. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  1480. $fields .= urlencode(':Model.hidden|Model.id');
  1481. $this->Controller->request = $this->Controller->request->withParsedBody([
  1482. 'Model' => [
  1483. 'hidden' => 'tampered',
  1484. 'id' => '1',
  1485. ],
  1486. '_Token' => compact('fields', 'unlocked', 'debug'),
  1487. ]);
  1488. $result = $this->validatePost('SecurityException', 'Tampered field \'Model.hidden\' in POST data (expected value \'value\' but found \'tampered\')');
  1489. $this->assertFalse($result);
  1490. }
  1491. /**
  1492. * testValidatePostFailTamperingMutatedIntoArray method
  1493. *
  1494. * Test that validatePost fails with tampered fields and explanation.
  1495. *
  1496. * @return void
  1497. * @triggers Controller.startup $this->Controller
  1498. */
  1499. public function testValidatePostFailTamperingMutatedIntoArray()
  1500. {
  1501. $event = new Event('Controller.startup', $this->Controller);
  1502. $this->Security->startup($event);
  1503. $unlocked = '';
  1504. $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
  1505. $debug = urlencode(json_encode([
  1506. '/articles/index',
  1507. $fields,
  1508. [],
  1509. ]));
  1510. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  1511. $fields .= urlencode(':Model.hidden|Model.id');
  1512. $this->Controller->request = $this->Controller->request->withData('Model', [
  1513. 'hidden' => ['some-key' => 'some-value'],
  1514. 'id' => '1',
  1515. ])->withData('_Token', compact('fields', 'unlocked', 'debug'));
  1516. $result = $this->validatePost(
  1517. 'SecurityException',
  1518. 'Unexpected field \'Model.hidden.some-key\' in POST data, Missing field \'Model.hidden\' in POST data'
  1519. );
  1520. $this->assertFalse($result);
  1521. }
  1522. /**
  1523. * testValidatePostUnexpectedDebugToken method
  1524. *
  1525. * Test that debug token should not be sent if debug is disabled.
  1526. *
  1527. * @return void
  1528. * @triggers Controller.startup $this->Controller
  1529. */
  1530. public function testValidatePostUnexpectedDebugToken()
  1531. {
  1532. $event = new Event('Controller.startup', $this->Controller);
  1533. $this->Security->startup($event);
  1534. $unlocked = '';
  1535. $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
  1536. $debug = urlencode(json_encode([
  1537. '/articles/index',
  1538. $fields,
  1539. [],
  1540. ]));
  1541. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  1542. $fields .= urlencode(':Model.hidden|Model.id');
  1543. $this->Controller->request = $this->Controller->request->withParsedBody([
  1544. 'Model' => [
  1545. 'hidden' => ['some-key' => 'some-value'],
  1546. 'id' => '1',
  1547. ],
  1548. '_Token' => compact('fields', 'unlocked', 'debug'),
  1549. ]);
  1550. Configure::write('debug', false);
  1551. $result = $this->validatePost('SecurityException', 'Unexpected \'_Token.debug\' found in request data');
  1552. $this->assertFalse($result);
  1553. }
  1554. /**
  1555. * testAuthRequiredThrowsExceptionTokenNotFoundPost method
  1556. *
  1557. * Auth required throws exception token not found.
  1558. *
  1559. * @group deprecated
  1560. * @return void
  1561. * @triggers Controller.startup $this->Controller
  1562. */
  1563. public function testAuthRequiredThrowsExceptionTokenNotFoundPost()
  1564. {
  1565. $this->expectException(\Cake\Controller\Exception\AuthSecurityException::class);
  1566. $this->expectExceptionMessage('\'_Token\' was not found in request data.');
  1567. $this->deprecated(function () {
  1568. $this->Security->setConfig('requireAuth', ['protected']);
  1569. $this->Controller->request->params['action'] = 'protected';
  1570. $this->Controller->request->data = 'notEmpty';
  1571. $this->Security->authRequired($this->Controller);
  1572. });
  1573. }
  1574. /**
  1575. * testAuthRequiredThrowsExceptionTokenNotFoundSession method
  1576. *
  1577. * Auth required throws exception token not found in Session.
  1578. *
  1579. * @group deprecated
  1580. * @return void
  1581. * @triggers Controller.startup $this->Controller
  1582. */
  1583. public function testAuthRequiredThrowsExceptionTokenNotFoundSession()
  1584. {
  1585. $this->expectException(\Cake\Controller\Exception\AuthSecurityException::class);
  1586. $this->expectExceptionMessage('\'_Token\' was not found in session.');
  1587. $this->deprecated(function () {
  1588. $this->Security->setConfig('requireAuth', ['protected']);
  1589. $this->Controller->request->params['action'] = 'protected';
  1590. $this->Controller->request->data = ['_Token' => 'not empty'];
  1591. $this->Security->authRequired($this->Controller);
  1592. });
  1593. }
  1594. /**
  1595. * testAuthRequiredThrowsExceptionControllerNotAllowed method
  1596. *
  1597. * Auth required throws exception controller not allowed.
  1598. *
  1599. * @group deprecated
  1600. * @return void
  1601. * @triggers Controller.startup $this->Controller
  1602. */
  1603. public function testAuthRequiredThrowsExceptionControllerNotAllowed()
  1604. {
  1605. $this->expectException(\Cake\Controller\Exception\AuthSecurityException::class);
  1606. $this->expectExceptionMessage('Controller \'NotAllowed\' was not found in allowed controllers: \'Allowed, AnotherAllowed\'.');
  1607. $this->deprecated(function () {
  1608. $this->Security->setConfig('requireAuth', ['protected']);
  1609. $this->Controller->request->params['controller'] = 'NotAllowed';
  1610. $this->Controller->request->params['action'] = 'protected';
  1611. $this->Controller->request->data = ['_Token' => 'not empty'];
  1612. $this->Controller->request->session()->write('_Token', [
  1613. 'allowedControllers' => ['Allowed', 'AnotherAllowed'],
  1614. ]);
  1615. $this->Security->authRequired($this->Controller);
  1616. });
  1617. }
  1618. /**
  1619. * testAuthRequiredThrowsExceptionActionNotAllowed method
  1620. *
  1621. * Auth required throws exception controller not allowed.
  1622. *
  1623. * @return void
  1624. * @triggers Controller.startup $this->Controller
  1625. */
  1626. public function testAuthRequiredThrowsExceptionActionNotAllowed()
  1627. {
  1628. $this->expectException(\Cake\Controller\Exception\AuthSecurityException::class);
  1629. $this->expectExceptionMessage('Action \'NotAllowed::protected\' was not found in allowed actions: \'index, view\'.');
  1630. $this->deprecated(function () {
  1631. $this->Security->setConfig('requireAuth', ['protected']);
  1632. $this->Controller->request->params['controller'] = 'NotAllowed';
  1633. $this->Controller->request->params['action'] = 'protected';
  1634. $this->Controller->request->data = ['_Token' => 'not empty'];
  1635. $this->Controller->request->session()->write('_Token', [
  1636. 'allowedActions' => ['index', 'view'],
  1637. ]);
  1638. $this->Security->authRequired($this->Controller);
  1639. });
  1640. }
  1641. /**
  1642. * testAuthRequired method
  1643. *
  1644. * Auth required throws exception controller not allowed.
  1645. *
  1646. * @return void
  1647. * @triggers Controller.startup $this->Controller
  1648. */
  1649. public function testAuthRequired()
  1650. {
  1651. $this->deprecated(function () {
  1652. $this->Security->setConfig('requireAuth', ['protected']);
  1653. $this->Controller->request->params['controller'] = 'Allowed';
  1654. $this->Controller->request->params['action'] = 'protected';
  1655. $this->Controller->request->data = ['_Token' => 'not empty'];
  1656. $this->Controller->request->session()->write('_Token', [
  1657. 'allowedActions' => ['protected'],
  1658. 'allowedControllers' => ['Allowed'],
  1659. ]);
  1660. $this->assertTrue($this->Security->authRequired($this->Controller));
  1661. });
  1662. }
  1663. }