Skip to content

Commit

Permalink
System: add nonce and CSRF token handling to all POST forms (#1891)
Browse files Browse the repository at this point in the history
Co-authored-by: Sandra Kuipers <[email protected]>
  • Loading branch information
Ali Alam and SKuipers authored Feb 13, 2025
1 parent a5e03e9 commit 4585051
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 35 deletions.
18 changes: 15 additions & 3 deletions gibbon.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
*/

use Gibbon\Http\Url;
use Gibbon\Data\Validator;
use Gibbon\Session\TokenHandler;

// Handle fatal errors more gracefully
register_shutdown_function(function () {
Expand Down Expand Up @@ -144,16 +146,26 @@
}

// Sanitize incoming user-supplied GET variables
$validator = $container->get(\Gibbon\Data\Validator::class);
$validator = $container->get(Validator::class);
$_GET = $validator->sanitizeUrlParams($_GET);
$tokenHandler = $container->get(TokenHandler::class);

// Check for CSRF token when posting any form
// Check for CSRF token and nonce when posting any form
if (!empty($_POST) && stripos($_SERVER['PHP_SELF'], 'Process.php') !== false) {
if (!$validator->validateToken()) {

// Validate CSRF token
if (!$tokenHandler->validateCsrfToken()) {
$URL .= $_SERVER['HTTP_REFERER'].'&return=error9';
header("Location: {$URL}");
exit;
}

// Validate nonce
if (!$tokenHandler->validateNonce()) {
$URL .= $_SERVER['HTTP_REFERER'].'&return=error10';
header("Location: {$URL}");
exit;
}
}


11 changes: 2 additions & 9 deletions src/Data/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@
*/
class Validator
{
protected $csrfToken;
protected $allowableHTML;
protected $allowableHTMLString;
protected $allowableIframeSources;

public function __construct(string $allowableHTMLString, string $allowableIframeSources = '', string $csrfToken = '')
public function __construct(string $allowableHTMLString, string $allowableIframeSources = '')
{
$this->allowableHTMLString = $allowableHTMLString;
$this->allowableHTML = $this->parseTagsFromString($this->allowableHTMLString);
$this->allowableIframeSources = explode(',', mb_strtolower($allowableIframeSources));
$this->csrfToken = $csrfToken;
}

public function getAllowableHTML()
Expand All @@ -52,12 +50,7 @@ public function getAllowableIframeSources()
{
return $this->allowableIframeSources;
}

public function validateToken()
{
return !empty($this->csrfToken) && $this->csrfToken === $_POST['token'];
}


/**
* Sanitize the input data.
*
Expand Down
11 changes: 7 additions & 4 deletions src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@

use Gibbon\Http\Url;
use Gibbon\Tables\Action;
use Gibbon\Session\TokenHandler;
use Gibbon\Forms\View\FormBlankView;
use Gibbon\Forms\View\FormTableView;
use Gibbon\Forms\FormFactoryInterface;
use League\Container\ContainerAwareTrait;
use Gibbon\Forms\View\FormRendererInterface;
use Gibbon\Forms\Traits\BasicAttributesTrait;
use League\Container\ContainerAwareTrait;

/**
* Form
Expand Down Expand Up @@ -91,9 +92,11 @@ public static function create($id, $action, $method = 'post', $class = 'standard
->setAction($action)
->setMethod($method);

// Add the CSRF token to all forms
$form->addHiddenValue('token', $container->has('token') ? $container->get('token') : '');

// Add the CSRF and Nonce tokens to all forms
$tokenHandler = $container->get(TokenHandler::class);
$form->addHiddenValue('token', $tokenHandler->getCSRF());
$form->addHiddenValue('nonce', $tokenHandler->getNonce());

// Enable quick save by default on edit and settings pages
if ($form->checkActionList($action, ['settingsProcess', 'editProcess'])) {
$form->enableQuickSave();
Expand Down
6 changes: 1 addition & 5 deletions src/Services/CoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,6 @@ public function register()
return $page;
});

$container->share('token', function () {
return $this->getLeagueContainer()->get('session')->get('token');
});

$container->add(MailerInterface::class, function () use ($container) {
$view = new View($container->get('twig'));
return (new Mailer($container->get('session')))->setView($view);
Expand All @@ -287,7 +283,7 @@ public function register()

$container->share(Validator::class, function () {
$session = $this->getLeagueContainer()->get('session');
return new Validator($session->get('allowableHTML', ''), $session->get('allowableIframeSources', ''), $session->get('token', ''));
return new Validator($session->get('allowableHTML', ''), $session->get('allowableIframeSources', ''));
});

$container->add(PasswordPolicy::class, function () use ($container) {
Expand Down
14 changes: 2 additions & 12 deletions src/Session/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,7 @@ class Session implements SessionInterface
* @param string $action
* Optional string of the current action.
*/
public function __construct(
string $guid,
string $address = '',
string $module = '',
string $action = ''
)
public function __construct(string $guid, string $address = '', string $module = '', string $action = '')
{
// Backwards compatibility for external modules.
$this->guid = $guid;
Expand All @@ -64,13 +59,8 @@ public function __construct(
$this->set('address', $address);
$this->set('module', $module);
$this->set('action', $action);

// Create a CSRF token
if (!$this->exists('token')) {
$this->set('token', bin2hex(random_bytes(16)));
}
}

public function setGuid(string $_guid)
{
$this->guid = $_guid;
Expand Down
108 changes: 108 additions & 0 deletions src/Session/TokenHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/*
Gibbon: the flexible, open school platform
Founded by Ross Parker at ICHK Secondary. Built by Ross Parker, Sandra Kuipers and the Gibbon community (https://gibbonedu.org/about/)
Copyright © 2010, Gibbon Foundation
Gibbon™, Gibbon Education Ltd. (Hong Kong)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace Gibbon\Session;

use Gibbon\Contracts\Services\Session as SessionInterface;

/**
* tokenHandler Class
*
* @version v29
* @since v29
*/

class TokenHandler {

/**
* @var SessionInterface
*/
private $session;

public function __construct(SessionInterface $session)
{
$this->session = $session;

// Create a CSRF token
if (!$session->exists('token')) {
$session->set('token', bin2hex(random_bytes(16)));
}

// Create a nonce list
if (!$session->exists('nonceList')) {
$session->set('nonceList', []);
}
}

public function getCSRF()
{
return $this->session->get('token');
}

public function validateCsrfToken()
{
return !empty($this->session->get('token')) && $this->session->get('token') === $_POST['token'];
}


public function validateNonce()
{
$nonce = $_POST['nonce'] ?? '';

if(empty($nonce)) {
return false;
}

return $this->removeNonce($nonce);
}

/**
* Create and add a nonce to the session's nonce list, then return it.
*/
public function getNonce()
{
$nonce = bin2hex(random_bytes(16));

$nonceList = $this->session->get('nonceList', []);
$nonceList[] = $nonce;
$this->session->set('nonceList', $nonceList);

return $nonce;
}

/**
* Check if a nonce exists when a form is submitted. If yes, then remove it from the list.
*
* @param string $nonce
* @return bool
*/
public function removeNonce(string $nonce) {
$nonceList = $this->session->get('nonceList', []);

if (($key = array_search($nonce, $nonceList)) !== false) {
unset($nonceList[$key]);
$this->session->set('nonceList', array_values($nonceList));
return true;
}
return false;
}

}
1 change: 1 addition & 0 deletions src/View/Components/ReturnMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function __construct()
'error7' => __('Your request failed because some required values were not unique.'),
'error8' => __('Your request failed because the link is invalid or has expired.'),
'error9' => __('Your request failed because your session authentication has expired. Please log out and log in again.'),
'error10' => __('Your request failed because you are trying to re-submit a form that has been submitted before. Please submit a new form.'),

//Warnings
'warning0' => __('Your optional extra data failed to save.'),
Expand Down
11 changes: 9 additions & 2 deletions tests/unit/Forms/FormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@

use PHPUnit\Framework\TestCase;
use Gibbon\Forms\View\FormRendererInterface;
use Gibbon\Session\TokenHandler;
use Gibbon\Services\ViewServiceProvider;
use League\Container\Container;
use Gibbon\Forms\View\FormView;
use Gibbon\Contracts\Services\Session as SessionInterface;
use Gibbon\Session\Session;

/**
* @covers Form
Expand Down Expand Up @@ -59,8 +62,12 @@ public function setUp(): void
return $twig;
});

$container->share('token', 'test-token-value');

$container->share(SessionInterface::class, function () {
return new Session('test-guid');
});
$container->share(TokenHandler::class, function () use ($container) {
return new TokenHandler($container->get(SessionInterface::class));
});
$service = new ViewServiceProvider();
$service->setContainer($container);
$service->register();
Expand Down

0 comments on commit 4585051

Please sign in to comment.