fixes #237: adding CSRF token to all forms and requires now on POST
parent
b2ed873b1b
commit
98faef1b06
|
@ -10,7 +10,7 @@ not yet released
|
|||
* #169: Increased server ip char limit to 500.
|
||||
* #164: Added support for FreeVoipDeal SMS gateway <http://www.freevoipdeal.com>.
|
||||
* #181: Added blank index files to prevent directory listing.
|
||||
|
||||
* #237: Adding CSRF protection.
|
||||
|
||||
v3.1.1 (released November 6, 2014)
|
||||
----------------------------------
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
"symfony/event-dispatcher": "2.8.*",
|
||||
"symfony/http-foundation": "2.8.*",
|
||||
"php-pushover/php-pushover": "dev-master",
|
||||
"twig/twig": "1.*"
|
||||
"twig/twig": "1.*",
|
||||
"paragonie/random_compat" : "1.1.6",
|
||||
"indigophp/hash-compat" : "1.1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
|
|
@ -4,9 +4,60 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "f30b2f3412fa5a525947648bb15618e8",
|
||||
"content-hash": "3fe8378a69be2b650919ad9cff79690d",
|
||||
"hash": "8f27400edd82e99aa35998a3e01fc23e",
|
||||
"content-hash": "3d1c36ee7e11634bc149bfc9a250e4ae",
|
||||
"packages": [
|
||||
{
|
||||
"name": "indigophp/hash-compat",
|
||||
"version": "v1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/indigophp/hash-compat.git",
|
||||
"reference": "43a19f42093a0cd2d11874dff9d891027fc42214"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/indigophp/hash-compat/zipball/43a19f42093a0cd2d11874dff9d891027fc42214",
|
||||
"reference": "43a19f42093a0cd2d11874dff9d891027fc42214",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.2-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/hash_equals.php",
|
||||
"src/hash_pbkdf2.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Backports hash_* functionality to older PHP versions",
|
||||
"homepage": "https://indigophp.com",
|
||||
"keywords": [
|
||||
"hash",
|
||||
"hash_equals",
|
||||
"hash_pbkdf2"
|
||||
],
|
||||
"time": "2015-08-22 07:03:35"
|
||||
},
|
||||
{
|
||||
"name": "ircmaxell/password-compat",
|
||||
"version": "v1.0.4",
|
||||
|
@ -49,6 +100,54 @@
|
|||
],
|
||||
"time": "2014-11-20 16:49:30"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "e6f80ab77885151908d0ec743689ca700886e8b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/e6f80ab77885151908d0ec743689ca700886e8b0",
|
||||
"reference": "e6f80ab77885151908d0ec743689ca700886e8b0",
|
||||
"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": "2016-01-29 16:19:52"
|
||||
},
|
||||
{
|
||||
"name": "php-pushover/php-pushover",
|
||||
"version": "dev-master",
|
||||
|
|
|
@ -67,6 +67,11 @@ abstract class AbstractController extends ContainerAware implements ControllerIn
|
|||
*/
|
||||
protected $add_menu = true;
|
||||
|
||||
/**
|
||||
* @var string $csrf_key
|
||||
*/
|
||||
protected $csrf_key;
|
||||
|
||||
/**
|
||||
* Messages to show the user
|
||||
* @var array $messages
|
||||
|
@ -470,4 +475,23 @@ abstract class AbstractController extends ContainerAware implements ControllerIn
|
|||
public function getUser() {
|
||||
return $this->container->get('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom key for CSRF validation
|
||||
* @return string
|
||||
*/
|
||||
public function getCSRFKey() {
|
||||
return $this->csrf_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CSRF key for validation
|
||||
* @param string $key
|
||||
* @return \psm\Module\ControllerInterface
|
||||
*/
|
||||
protected function setCSRFKey($key) {
|
||||
$this->csrf_key = $key;
|
||||
$this->twig->addGlobal('csrf_key', $key);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ class ConfigController extends AbstractController {
|
|||
parent::__construct($db, $twig);
|
||||
|
||||
$this->setMinUserLevelRequired(PSM_USER_ADMIN);
|
||||
$this->setCSRFKey('config');
|
||||
|
||||
$this->setActions(array(
|
||||
'index', 'save',
|
||||
|
|
|
@ -44,4 +44,10 @@ interface ControllerInterface extends ContainerAwareInterface {
|
|||
* @return int
|
||||
*/
|
||||
public function getMinUserLevelRequired();
|
||||
|
||||
/**
|
||||
* Get custom key for CSRF validation
|
||||
* @return string
|
||||
*/
|
||||
public function getCSRFKey();
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ class InstallController extends AbstractController {
|
|||
parent::__construct($db, $twig);
|
||||
|
||||
$this->setMinUserLevelRequired(PSM_USER_ANONYMOUS);
|
||||
$this->setCSRFKey('install');
|
||||
$this->addMenu(false);
|
||||
|
||||
$this->path_config = PSM_PATH_SRC . '../config.php';
|
||||
|
|
|
@ -44,6 +44,7 @@ class ServerController extends AbstractServerController {
|
|||
|
||||
$this->server_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
$this->setCSRFKey('server');
|
||||
$this->setActions(array(
|
||||
'index', 'edit', 'save', 'delete', 'view',
|
||||
), 'index');
|
||||
|
|
|
@ -36,6 +36,7 @@ class LoginController extends AbstractController {
|
|||
parent::__construct($db, $twig);
|
||||
|
||||
$this->setMinUserLevelRequired(PSM_USER_ANONYMOUS);
|
||||
$this->setCSRFKey('login');
|
||||
|
||||
$this->setActions(array(
|
||||
'login', 'forgot', 'reset',
|
||||
|
|
|
@ -43,6 +43,7 @@ class ProfileController extends AbstractController {
|
|||
$this->setActions(array(
|
||||
'index', 'save',
|
||||
), 'index');
|
||||
$this->setCSRFKey('profile');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,6 +40,7 @@ class UserController extends AbstractController {
|
|||
parent::__construct($db, $twig);
|
||||
|
||||
$this->setMinUserLevelRequired(PSM_USER_ADMIN);
|
||||
$this->setCSRFKey('user');
|
||||
|
||||
$this->setActions(array(
|
||||
'index', 'edit', 'delete', 'save',
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
namespace psm;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||
|
@ -51,6 +52,7 @@ class Router {
|
|||
|
||||
public function __construct() {
|
||||
$this->container = $this->buildServiceContainer();
|
||||
$this->buildTwigEnvironment();
|
||||
|
||||
$mods = $this->container->getParameter('modules');
|
||||
|
||||
|
@ -72,8 +74,6 @@ class Router {
|
|||
* @throws \LogicException
|
||||
*/
|
||||
public function run($mod) {
|
||||
$user = $this->container->get('user');
|
||||
|
||||
if(strpos($mod, '_') !== false) {
|
||||
list($mod, $controller) = explode('_', $mod);
|
||||
} else {
|
||||
|
@ -81,18 +81,21 @@ class Router {
|
|||
}
|
||||
|
||||
$controller = $this->getController($mod, $controller);
|
||||
|
||||
// get min required level for this controller and make sure the user matches
|
||||
$min_lvl = $controller->getMinUserLevelRequired();
|
||||
$action = null;
|
||||
|
||||
if($min_lvl < PSM_USER_ANONYMOUS) {
|
||||
// if user is not logged in, load login module
|
||||
if(!$user->isUserLoggedIn()) {
|
||||
try {
|
||||
$this->validateRequest($controller);
|
||||
} catch (\InvalidArgumentException $ex) {
|
||||
switch($ex->getMessage()) {
|
||||
case 'login_required':
|
||||
$controller = $this->getController('user', 'login');
|
||||
} elseif($user->getUserLevel() > $min_lvl) {
|
||||
break;
|
||||
case 'invalid_csrf_token':
|
||||
case 'invalid_user_level':
|
||||
default:
|
||||
$controller = $this->getController('error');
|
||||
$action = '401';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,6 +148,48 @@ class Router {
|
|||
return $this->container->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate requets before heading to a controller
|
||||
* @param \psm\Module\ControllerInterface $controller
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function validateRequest(\psm\Module\ControllerInterface $controller) {
|
||||
$request = Request::createFromGlobals();
|
||||
|
||||
if($request->getMethod() == 'POST') {
|
||||
// require CSRF token for all POST calls
|
||||
$session = $this->container->get('user')->getSession();
|
||||
$token_in = $request->request->get('csrf', '');
|
||||
$csrf_key = $controller->getCSRFKey();
|
||||
|
||||
if(empty($csrf_key)) {
|
||||
if(!hash_equals($session->get('csrf_token'), $token_in)) {
|
||||
throw new \InvalidArgumentException('invalid_csrf_token');
|
||||
}
|
||||
} else {
|
||||
if(!hash_equals(
|
||||
hash_hmac('sha256', $csrf_key, $session->get('csrf_token2')),
|
||||
$token_in
|
||||
)) {
|
||||
throw new \InvalidArgumentException('invalid_csrf_token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get min required level for this controller and make sure the user matches
|
||||
$min_lvl = $controller->getMinUserLevelRequired();
|
||||
|
||||
if($min_lvl < PSM_USER_ANONYMOUS) {
|
||||
// if user is not logged in, load login module
|
||||
if(!$this->container->get('user')->isUserLoggedIn()) {
|
||||
throw new \InvalidArgumentException('login_required');
|
||||
} elseif($this->container->get('user')->getUserLevel() > $min_lvl) {
|
||||
throw new \InvalidArgumentException('invalid_user_level');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build a new service container
|
||||
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
|
||||
|
@ -157,4 +202,32 @@ class Router {
|
|||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare twig environment
|
||||
* @return \Twig_Environment
|
||||
*/
|
||||
protected function buildTwigEnvironment() {
|
||||
$twig = $this->container->get('twig');
|
||||
$session = $this->container->get('user')->getSession();
|
||||
if(!$session->has('csrf_token')) {
|
||||
$session->set('csrf_token', bin2hex(random_bytes(32)));
|
||||
}
|
||||
if(!$session->has('csrf_token2')) {
|
||||
$session->set('csrf_token2', random_bytes(32));
|
||||
}
|
||||
|
||||
$twig->addFunction(
|
||||
new \Twig_SimpleFunction(
|
||||
'csrf_token',
|
||||
function($lock_to = null) use ($session) {
|
||||
if(empty($lock_to)) {
|
||||
return $session->get('csrf_token');
|
||||
}
|
||||
return hash_hmac('sha256', $lock_to, $session->get('csrf_token2'));
|
||||
}
|
||||
)
|
||||
);
|
||||
return $twig;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{% macro csrf_input() %}
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token(csrf_key|default('')) }}" />
|
||||
{% endmacro %}
|
|
@ -1,3 +1,4 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<form class="form-horizontal" name="edit_config" action="index.php?mod=config&action=save" id="edit_config" method="post">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="{{ general_active }}"><a href="#config-general" data-toggle="tab">{{ label_general }}</a></li>
|
||||
|
@ -225,4 +226,5 @@
|
|||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
{{ macro.csrf_input() }}
|
||||
</form>
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "module/install/main.tpl.html" %}
|
||||
{% use "module/install/results.tpl.html" %}
|
||||
{% import 'main/macros.tpl.html' as macro %}
|
||||
|
||||
{% block install %}
|
||||
<div class="row-fluid">
|
||||
|
@ -8,6 +9,7 @@
|
|||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<form id="psm_config" class="form-horizontal" action="install.php?action=config" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<p>Please enter your database info:</p>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="host">Database host</label>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "module/install/main.tpl.html" %}
|
||||
{% use "module/install/results.tpl.html" %}
|
||||
{% import 'main/macros.tpl.html' as macro %}
|
||||
|
||||
{% block install %}
|
||||
<p>Sweet, your database connection is up and running!</p>
|
||||
|
@ -9,6 +10,7 @@
|
|||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<form id="psm_config" class="form-horizontal" action="install.php?action=install" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="username">Username</label>
|
||||
<div class="controls">
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<form class="form-horizontal well" action="{{ url_save|raw }}" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<fieldset>
|
||||
<legend>{{ titlemode }}</legend>
|
||||
<div class="control-group">
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<div class="container">
|
||||
<form class="form-signin" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<h3 class="form-signin-heading">{{ title_forgot }}</h3>
|
||||
<input type="text" name="user_name" class="input-block-level" placeholder="{{ label_username }}" value="{{ value_user_name }}" required>
|
||||
<button class="btn btn-primary" type="submit">{{ label_submit }}</button>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<div class="container">
|
||||
<form class="form-signin" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<h3 class="form-signin-heading">{{ title_sign_in }}</h3>
|
||||
<input type="text" name="user_name" class="input-block-level" placeholder="{{ label_username }}" value="{{ value_user_name }}" required>
|
||||
<input type="password" name="user_password" class="input-block-level" placeholder="{{ label_password }}" required>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<div class="container">
|
||||
<form class="form-signin" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<h3 class="form-signin-heading">{{ title_reset }}</h3>
|
||||
<input type="text" name="user_name" class="input-block-level" placeholder="{{ label_username }}" value="{{ value_user_name }}" required disabled="disabled">
|
||||
<input type="password" name="user_password_new" class="input-block-level" placeholder="{{ label_password }}" required autocomplete="off">
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<form class="form-horizontal well" action="{{ form_action|raw }}" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<fieldset>
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% import 'main/macros.tpl.html' as macro %}
|
||||
<form class="form-horizontal well" action="{{ url_save|raw }}" method="post">
|
||||
{{ macro.csrf_input() }}
|
||||
<fieldset>
|
||||
<legend>{{ titlemode }}</legend>
|
||||
<div class="control-group">
|
||||
|
|
Loading…
Reference in New Issue