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);
}