Skip to content

Commit

Permalink
feat(security): enforce csrf protection
Browse files Browse the repository at this point in the history
Enforce csrf protection on unsafe http requests (POST, PUT, DELETE, PATCH)

refactor: create Slim app using di container
  • Loading branch information
dfranco committed Dec 27, 2023
1 parent 2c5d4b9 commit 41cdcb6
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 26 deletions.
41 changes: 41 additions & 0 deletions application/CsrfErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/**
* Copyright (C) 2023 Davide Franco
*
* This file is part of Bacula-Web.
*
* Bacula-Web 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 2 of the License, or
* (at your option) any later version.
*
* Bacula-Web 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 Bacula-Web. If not, see
* <https://www.gnu.org/licenses/>.
*/

namespace App;

use Closure;
use Psr\Http\Message\ResponseFactoryInterface;

class CsrfErrorHandler
{
/**
* @param ResponseFactoryInterface $responseFactory
* @return Closure
*/
public function handle(ResponseFactoryInterface $responseFactory): Closure
{
return function () use ($responseFactory) {
$response = $responseFactory->createResponse()->withStatus(403);
$response->getBody()->write('Invalid CSRF token, go back to <a href="/">Home page</a>');
return $response;
};
}
}
72 changes: 72 additions & 0 deletions application/Middleware/CsrfMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* Copyright (C) 2010-2023 Davide Franco
*
* This file is part of Bacula-Web.
*
* Bacula-Web 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 2 of the License, or
* (at your option) any later version.
*
* Bacula-Web 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 Bacula-Web. If not, see
* <https://www.gnu.org/licenses/>.
*/

namespace App\Middleware;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Csrf\Guard;
use Slim\Views\Twig;

class CsrfMiddleware implements MiddlewareInterface
{
private Twig $twig;
private Guard $csrf;

/**
* @param Twig $twig
* @param ContainerInterface $container
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function __construct(Twig $twig, ContainerInterface $container)
{
$this->twig = $twig;
$this->csrf = $container->get('csrf');
}

/**
* @param Request $request
* @param RequestHandler $handler
* @return ResponseInterface
*/
public function process(Request $request, RequestHandler $handler): ResponseInterface
{
$csrfKeyName = $this->csrf->getTokenNameKey();
$csrfKeyValue = $this->csrf->getTokenValueKey();
$csrfTokenName = $this->csrf->getTokenName();
$csrfTokenValue = $this->csrf->getTokenValue();

$csrf = "
<input type='hidden' name='$csrfKeyName' value='$csrfTokenName'>
<input type='hidden' name='$csrfKeyValue' value='$csrfTokenValue'>
";

$this->twig->getEnvironment()->addGlobal('csrf', $csrf);

return $handler->handle($request);
}
}
37 changes: 27 additions & 10 deletions application/config/container-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use App\CsrfErrorHandler;
use App\Libs\FileConfig;
use App\Tables\ClientTable;
use App\Tables\JobFileTable;
Expand All @@ -14,6 +15,10 @@
use Odan\Session\SessionInterface;
use Odan\Session\SessionManagerInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Slim\App;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Twig\Extension\DebugExtension;
use Symfony\Component\Translation\Translator;
Expand All @@ -33,35 +38,47 @@
'cookie_samesite' => 'Lax'
]
],
JobTable::class => function(SessionInterface $session) {
return new JobTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
App::class => function (ContainerInterface $container) {
AppFactory::setContainer($container);
return AppFactory::create();
},
PoolTable::class => function(SessionInterface $session) {
ResponseFactoryInterface::class => function (App $app) {
return $app->getResponseFactory();
},
'csrf' => function(ResponseFactoryInterface $responseFactory, CsrfErrorHandler $csrf) {
return new Guard(
$responseFactory,
failureHandler: $csrf->handle($responseFactory),
persistentTokenMode: false);
},
JobTable::class => function (SessionInterface $session) {
return new JobTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
PoolTable::class => function (SessionInterface $session) {
return new PoolTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
ClientTable::class => function(SessionInterface $session) {
ClientTable::class => function (SessionInterface $session) {
return new ClientTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
VolumeTable::class => function(SessionInterface $session) {
VolumeTable::class => function (SessionInterface $session) {
return new VolumeTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
JobFileTable::class => function(SessionInterface $session) {
JobFileTable::class => function (SessionInterface $session) {
return new JobFileTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
LogTable::class => function(SessionInterface $session) {
LogTable::class => function (SessionInterface $session) {
return new LogTable(DatabaseFactory::getDatabase($session->get('catalog_id', 0)));
},
SessionManagerInterface::class => function (ContainerInterface $container) {
return $container->get(SessionInterface::class);
},

SessionInterface::class => function (ContainerInterface $container) {
$options = $container->get('settings')['session'];

return new PhpSession($options);
},
Twig::class => function (ContainerInterface $container, SessionInterface $session) {
$twig = Twig::create( BW_ROOT . '/application/views/templates', ['cache' => false]);
$twig = Twig::create(BW_ROOT . '/application/views/templates', ['cache' => false]);

$twig->addExtension(new DebugExtension());

Expand All @@ -80,7 +97,7 @@

$translator = $container->get(Translator::class);
$twig->addExtension(new TranslationExtension($translator));

return $twig;
},
Translator::class => function (ContainerInterface $container) {
Expand Down
2 changes: 2 additions & 0 deletions application/views/templates/pages/backupjob-report.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<div class="form-group pull-right">
<button type="submit" class="btn btn-primary">{{ 'View report'|trans }}</button>
</div>

{{ csrf|raw }}
</form>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions application/views/templates/pages/client-report.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<div class="mb-3 pull-right">
<button type="submit" class="btn btn-primary">{{ 'View report'|trans }}</button>
</div>
{{ csrf|raw }}
</form>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions application/views/templates/pages/dashboard.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
{{ period.label }}</option>
{% endfor %}
</select>

<button title="{{ 'Update with selected period'|trans }}" class="btn btn-primary btn-sm"
type="submit">{{ 'Submit'|trans }}</button>

{{ csrf|raw }}
</div>
</form>
</div>
Expand All @@ -51,6 +54,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="1" />
<button class="btn btn-lg btn-link type="submit">{{ running_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand All @@ -60,6 +64,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="3" />
<button class="btn btn-lg btn-link type="submit">{{ completed_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand All @@ -69,6 +74,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="4" />
<button class="btn btn-lg btn-link type="submit">{{ completed_with_errors_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand All @@ -78,6 +84,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="2" />
<button class="btn btn-lg btn-link type="submit">{{ waiting_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand All @@ -87,6 +94,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="5" />
<button class="btn btn-lg btn-link type="submit">{{ failed_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand All @@ -96,6 +104,7 @@
<form action="{{ url_for('jobs') }}" method="post">
<input type="hidden" name="filter_jobstatus" value="6" />
<button class="btn btn-lg btn-link type="submit">{{ canceled_jobs }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand Down Expand Up @@ -317,6 +326,7 @@
<form action="{{ url_for('backupjob') }}" method="post">
<input type="hidden" name="backupjob_name" value="{{ job.name }}" />
<button class="btn btn-link type="submit">{{ job.name }}</button>
{{ csrf|raw }}
</form>
</td>
<td class="text-right">{{ job.jobbytes }}</td>
Expand Down
2 changes: 2 additions & 0 deletions application/views/templates/pages/jobfiles.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<input type="hidden" name="backupjob_period" value="7"/>
<button type="submit"
class="btn btn-sm btn-primary">{{ 'View backup job'|trans }}</button>
{{ csrf|raw }}
</form>

</div>
Expand All @@ -49,6 +50,7 @@
<input type="hidden" name="jobId" value="{{ jobid }}">
<button type="submit" class="btn btn-primary">Search</button>
<button type="reset" class="btn btn-default" title="{{ 'Reset'|trans }}"></button>
{{ csrf|raw }}
</div>
</form>
</div>
Expand Down
2 changes: 2 additions & 0 deletions application/views/templates/pages/jobs.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@

<a class="btn btn-link btn-sm" title="{{ 'Reset to default'|trans }}" href="{{ base_path() }}/jobs"
role="button">{{ 'Reset to default'|trans }}</a>
{{ csrf|raw }}
</form>

</div>
Expand Down Expand Up @@ -144,6 +145,7 @@
<input type="hidden" name="backupjob_name" value="{{ job.job_name }}" />
<input type="hidden" name="backupjob_period" value="7" />
<button type="submit" class="btn btn-sm btn-link">{{ job.job_name }}</button>
{{ csrf|raw }}
</form>
{% else %}
{{ job.job_name }}
Expand Down
4 changes: 3 additions & 1 deletion application/views/templates/pages/login.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
<div class="input-group mb-3">
<span class="input-group-text">
<i class="cil-user"></i>&nbsp;
<input class="form-control" type="text" name="username" id="inputUsername" placeholder="Username" autofocus
<input class="form-control" type="text" name="username" id="inputUsername" placeholder="Username"
autofocus
required>
</div>
<div class="input-group mb-4"><span class="input-group-text">
Expand All @@ -30,6 +31,7 @@
<button class="btn btn-primary px-4" type="submit">Login</button>
</div>
</div>
{{ csrf|raw }}
</form>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions application/views/templates/pages/pools.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<form method="post" action="volumes">
<input type="hidden" name="filter_pool_id" value="{{ pool.poolid }}" />
<button class="btn btn-sm btn-primary" type="submit">{{ 'Show Volumes'|trans }}</button>
{{ csrf|raw }}
</form>
</td>
</tr>
Expand Down
2 changes: 2 additions & 0 deletions application/views/templates/pages/settings.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
<input type="text" class="form-control" id="config_basepath"
value="{{ config_basepath }}" disabled>
</div>
{{ csrf|raw }}
</form>
</div>

Expand Down Expand Up @@ -158,6 +159,7 @@
</div>

<button class="btn btn-sm btn-primary pull-right" type="submit">Create</button>
{{ csrf|raw }}
</form>

</div>
Expand Down
2 changes: 2 additions & 0 deletions application/views/templates/pages/usersettings.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</div>

<button class="btn btn-sm btn-primary pull-right" disabled="disabled" type="submit">Update</button>
{{ csrf|raw }}
</form>

</div>
Expand Down Expand Up @@ -64,6 +65,7 @@

<br/>
<button class="btn btn-sm btn-primary pull-right" type="submit">Reset password</button>
{{ csrf|raw }}
</div>
</form>
</div>
Expand Down
2 changes: 1 addition & 1 deletion application/views/templates/pages/volumes.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
<button type="submit" class="btn btn-primary btn-sm"
title="{{ 'Apply filter and options'|trans }}">{{ 'Apply'|trans }}</button>
</div>

{{ csrf|raw }}
</form>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions application/views/templates/partials/navbar.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<button type="submit" class="btn btn-sm btn-light ms-2" title="Sign out">
<i class="fa fa-sign-out fa-lg"></i> {{ 'Sign out'|trans }}
</button>
{{ csrf|raw }}
</form>
</li>
</ul>
Expand Down

0 comments on commit 41cdcb6

Please sign in to comment.