SecurityComponentTest.php 60 KB

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