diff --git a/composer.json b/composer.json index 90e36e05..24016281 100644 --- a/composer.json +++ b/composer.json @@ -81,6 +81,7 @@ "phploc/phploc": "~4.0.0", "jakub-onderka/php-parallel-lint": "~0.9.0", "sensiolabs/security-checker": "~4.0.0", + "paragonie/random_compat": "~2.0.0", "symfony/debug": "~3.4.0", "doctrine/instantiator": "~1.0.0", diff --git a/composer.lock b/composer.lock index 823538ba..11d3e97c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "88f485bcc21591dbaf905423dc70d835", + "content-hash": "8c2289ac491f1f1019f88be4f375b8a9", "packages": [ { "name": "behat/gherkin", @@ -1144,6 +1144,54 @@ "homepage": "http://www.oomphinc.com/", "time": "2017-03-31T16:57:39+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v2.0.11", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2017-09-27T21:40:39+00:00" + }, { "name": "pda/pheanstalk", "version": "v3.1.0", @@ -2091,16 +2139,16 @@ }, { "name": "psr/simple-cache", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", - "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", "shasum": "" }, "require": { @@ -2135,7 +2183,7 @@ "psr-16", "simple-cache" ], - "time": "2017-01-02T13:31:39+00:00" + "time": "2017-10-23T01:57:42+00:00" }, { "name": "robmorgan/phinx", @@ -3029,7 +3077,7 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -3086,7 +3134,7 @@ }, { "name": "symfony/cache", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", @@ -3156,7 +3204,7 @@ }, { "name": "symfony/config", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -3219,7 +3267,7 @@ }, { "name": "symfony/console", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -3288,7 +3336,7 @@ }, { "name": "symfony/css-selector", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -3341,7 +3389,7 @@ }, { "name": "symfony/debug", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -3397,16 +3445,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "752c45dc831dc42a472f0ab8ae0450b63b840656" + "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/752c45dc831dc42a472f0ab8ae0450b63b840656", - "reference": "752c45dc831dc42a472f0ab8ae0450b63b840656", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/12e901abc1cb0d637a0e5abe9923471361d96b07", + "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07", "shasum": "" }, "require": { @@ -3464,11 +3512,11 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-02-26T14:27:04+00:00" + "time": "2018-03-04T03:54:53+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -3524,7 +3572,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3587,7 +3635,7 @@ }, { "name": "symfony/filesystem", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3636,16 +3684,16 @@ }, { "name": "symfony/finder", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6a615613745cef820d807443f32076bb9f5d0a38" + "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6a615613745cef820d807443f32076bb9f5d0a38", - "reference": "6a615613745cef820d807443f32076bb9f5d0a38", + "url": "https://api.github.com/repos/symfony/finder/zipball/a479817ce0a9e4adfd7d39c6407c95d97c254625", + "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625", "shasum": "" }, "require": { @@ -3681,7 +3729,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-02-11T17:15:12+00:00" + "time": "2018-03-05T18:28:11+00:00" }, { "name": "symfony/polyfill-apcu", @@ -3800,7 +3848,7 @@ }, { "name": "symfony/process", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -3849,7 +3897,7 @@ }, { "name": "symfony/yaml", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -4004,7 +4052,7 @@ }, { "name": "symfony/var-dumper", - "version": "v3.4.5", + "version": "v3.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", diff --git a/src/Controller/GroupController.php b/src/Controller/GroupController.php index 56744723..ae7a06e2 100644 --- a/src/Controller/GroupController.php +++ b/src/Controller/GroupController.php @@ -92,9 +92,12 @@ class GroupController extends Controller } $form = new Form(); + $form->setMethod('POST'); $form->setAction(APP_URL . 'group/edit' . (!is_null($groupId) ? '/' . $groupId : '')); + $form->addField(new Form\Element\Csrf('group_form')); + $title = new Form\Element\Text('title'); $title->setContainerClass('form-group'); $title->setClass('form-control'); diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 25abafce..e1ceaafc 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -421,9 +421,9 @@ class ProjectController extends PHPCensor\Controller $form = new Form(); $form->setMethod('POST'); - $form->setAction(APP_URL.'project/' . $type); + $form->setAction(APP_URL . 'project/' . $type); - $form->addField(new Form\Element\Csrf('csrf')); + $form->addField(new Form\Element\Csrf('project_form')); $form->addField(new Form\Element\Hidden('pubkey')); $options = [ diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 66b7ef12..a1a84241 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -2,6 +2,7 @@ namespace PHPCensor\Controller; +use PHPCensor\Form\Element\Csrf; use PHPCensor\Helper\Email; use PHPCensor\Helper\Lang; use PHPCensor\Controller; @@ -38,6 +39,44 @@ class SessionController extends Controller $this->authentication = Service::getInstance(); } + protected function loginForm($values) + { + $form = new \PHPCensor\Form(); + $form->setMethod('POST'); + $form->setAction(APP_URL . 'session/login'); + + $form->addField(new Csrf('login_form')); + + $email = new \PHPCensor\Form\Element\Text('email'); + $email->setLabel(Lang::get('login')); + $email->setRequired(true); + $email->setContainerClass('form-group'); + $email->setClass('form-control'); + $form->addField($email); + + $pwd = new \PHPCensor\Form\Element\Password('password'); + $pwd->setLabel(Lang::get('password')); + $pwd->setRequired(true); + $pwd->setContainerClass('form-group'); + $pwd->setClass('form-control'); + $form->addField($pwd); + + $remember = \PHPCensor\Form\Element\Checkbox::create('remember_me', Lang::get('remember_me'), false); + $remember->setContainerClass('form-group'); + $remember->setCheckedValue(1); + $remember->setValue(0); + $form->addField($remember); + + $pwd = new \PHPCensor\Form\Element\Submit(); + $pwd->setValue(Lang::get('log_in')); + $pwd->setClass('btn-success'); + $form->addField($pwd); + + $form->setValues($values); + + return $form; + } + /** * Handles user login (form and processing) */ @@ -55,15 +94,22 @@ class SessionController extends Controller } } + $method = $this->request->getMethod(); + + if ($method === 'POST') { + $values = $this->getParams(); + } else { + $values = []; + } + + $form = $this->loginForm($values); + $isLoginFailure = false; - if ($this->request->getMethod() == 'POST') { - $token = $this->getParam('token'); - if (!isset($token, $_SESSION['login_token']) || $token !== $_SESSION['login_token']) { + if ($this->request->getMethod() === 'POST') { + if (!$form->getChild('login_form')->validate()) { $isLoginFailure = true; } else { - unset($_SESSION['login_token']); - $email = $this->getParam('email'); $password = $this->getParam('password', ''); $rememberMe = (bool)$this->getParam('remember_me', 0); @@ -116,41 +162,6 @@ class SessionController extends Controller } } - $form = new \PHPCensor\Form(); - $form->setMethod('POST'); - $form->setAction(APP_URL . 'session/login'); - - $email = new \PHPCensor\Form\Element\Text('email'); - $email->setLabel(Lang::get('login')); - $email->setRequired(true); - $email->setContainerClass('form-group'); - $email->setClass('form-control'); - $form->addField($email); - - $pwd = new \PHPCensor\Form\Element\Password('password'); - $pwd->setLabel(Lang::get('password')); - $pwd->setRequired(true); - $pwd->setContainerClass('form-group'); - $pwd->setClass('form-control'); - $form->addField($pwd); - - $remember = \PHPCensor\Form\Element\Checkbox::create('remember_me', Lang::get('remember_me'), false); - $remember->setContainerClass('form-group'); - $remember->setCheckedValue(1); - $remember->setValue(0); - $form->addField($remember); - - $pwd = new \PHPCensor\Form\Element\Submit(); - $pwd->setValue(Lang::get('log_in')); - $pwd->setClass('btn-success'); - $form->addField($pwd); - - $tokenValue = $this->generateToken(); - $_SESSION['login_token'] = $tokenValue; - $token = new \PHPCensor\Form\Element\Hidden('token'); - $token->setValue($tokenValue); - $form->addField($token); - $this->view->form = $form->render(); $this->view->failed = $isLoginFailure; @@ -261,20 +272,4 @@ class SessionController extends Controller return $rtn; } - - /** Generate a random token. - * - * @return string - */ - protected function generateToken() - { - if (function_exists('openssl_random_pseudo_bytes')) { - return bin2hex(openssl_random_pseudo_bytes(16)); - } - - return sprintf("%04x", mt_rand(0, 0xFFFF)) - . sprintf("%04x", mt_rand(0, 0xFFFF)) - . sprintf("%04x", mt_rand(0, 0xFFFF)) - . sprintf("%04x", mt_rand(0, 0xFFFF)); - } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index d50a6bc0..9381942a 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -84,9 +84,12 @@ class UserController extends Controller $this->layout->subtitle = Lang::get('edit_profile'); $form = new Form(); - $form->setAction(APP_URL.'user/profile'); + + $form->setAction(APP_URL . 'user/profile'); $form->setMethod('POST'); + $form->addField(new Form\Element\Csrf('profile_form')); + $name = new Form\Element\Text('name'); $name->setClass('form-control'); $name->setContainerClass('form-group'); @@ -159,15 +162,15 @@ class UserController extends Controller $method = $this->request->getMethod(); - if ($method == 'POST') { + if ($method === 'POST') { $values = $this->getParams(); } else { $values = []; } - $form = $this->userForm($values); + $form = $this->userForm($values); - if ($method != 'POST' || ($method == 'POST' && !$form->validate())) { + if ($method !== 'POST' || ($method == 'POST' && !$form->validate())) { $view = new View('User/edit'); $view->type = 'add'; $view->user = null; @@ -236,9 +239,11 @@ class UserController extends Controller protected function userForm($values, $type = 'add') { $form = new Form(); + $form->setMethod('POST'); - $form->setAction(APP_URL.'user/' . $type); - $form->addField(new Form\Element\Csrf('csrf')); + $form->setAction(APP_URL . 'user/' . $type); + + $form->addField(new Form\Element\Csrf('user_form')); $field = new Form\Element\Email('email'); $field->setRequired(true); @@ -281,6 +286,7 @@ class UserController extends Controller $form->addField($field); $form->setValues($values); + return $form; } diff --git a/src/Form/Element/Csrf.php b/src/Form/Element/Csrf.php index a6c4ce7a..2d7951a0 100644 --- a/src/Form/Element/Csrf.php +++ b/src/Form/Element/Csrf.php @@ -6,17 +6,16 @@ use PHPCensor\View; class Csrf extends Hidden { - /** - * @var integer - */ - protected $rows = 4; - /** * @return boolean */ public function validate() { - if ($this->value != $_COOKIE[$this->getName()]) { + $sessionToken = isset($_SESSION['csrf_tokens'][$this->getName()]) + ? $_SESSION['csrf_tokens'][$this->getName()] + : null; + + if ($this->value !== $sessionToken) { return false; } @@ -30,9 +29,12 @@ class Csrf extends Hidden { parent::onPreRender($view); - $csrf = md5(microtime(true)); - $view->csrf = $csrf; + $this->setValue( + rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=') + ); - setcookie($this->getName(), $csrf); + $view->value = $this->getValue(); + + $_SESSION['csrf_tokens'][$this->getName()] = $this->getValue(); } } diff --git a/src/View/Form/Csrf.phtml b/src/View/Form/Csrf.phtml index 3068e4f6..ae27cc6a 100644 --- a/src/View/Form/Csrf.phtml +++ b/src/View/Form/Csrf.phtml @@ -1 +1 @@ - + diff --git a/tests/src/FormTest.php b/tests/src/FormTest.php index 0344b8e1..ee66f04e 100755 --- a/tests/src/FormTest.php +++ b/tests/src/FormTest.php @@ -107,7 +107,7 @@ class FormTest extends \PHPUnit\Framework\TestCase public function testFieldSetBasics() { - $f = new Form\FieldSet(); + $f = new Form\FieldSet(); $f2 = new Form\FieldSet('group'); $f3 = new Form\FieldSet(); @@ -141,6 +141,14 @@ class FormTest extends \PHPUnit\Framework\TestCase $html = $f->render(); self::assertTrue(strpos($html, 'one') !== false); self::assertTrue(strpos($html, 'two') !== false); + + $children = $f->getChildren(); + self::assertEquals(2, count($children)); + self::assertEquals($f2, $children[$f2->getName()]); + self::assertEquals($f3, $children[$f3->getName()]); + + $child = $f->getChild($f3->getName()); + self::assertEquals($f3, $child); } public function testElements() @@ -196,6 +204,15 @@ class FormTest extends \PHPUnit\Framework\TestCase $e = new Form\Element\Url(); self::assertTrue(strpos($e->render(), 'url') !== false); + $_SESSION = []; + + $e = new Form\Element\Csrf(); + self::assertTrue(strpos($e->render(), $e->getValue()) !== false); + self::assertEquals($_SESSION['csrf_tokens'][$e->getName()], $e->getValue()); + self::assertTrue($e->validate()); + $e->setValue('111'); + self::assertFalse($e->validate()); + $e = new Form\Element\Password(); self::assertTrue(strpos($e->render(), 'password') !== false); }