Skip to content

Commit

Permalink
Added rate limiter to password protection form
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpapst committed Dec 4, 2024
1 parent b4e6469 commit 923e9d9
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Compatibility: requires minimum Kimai 2.25.0

- Added rate limiter to password protection form (10 failures within 1-hour will block access)
- Remove form target (password protection) to prevent proxy issues with http vs https
- Removed md5 password from session key
- Use non-deprecated API to fetch timesheets
Expand Down
4 changes: 2 additions & 2 deletions Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function indexAction(#[MapEntity(mapping: ['shareKey' => 'shareKey'])] Sh
$givenPassword = $request->get('spt-password');

// Check access.
if (!$this->viewService->hasAccess($sharedPortal, $givenPassword)) {
if (!$this->viewService->hasAccess($sharedPortal, $givenPassword, $request)) {
return $this->render('@CustomerPortal/view/auth.html.twig', [
'invalidPassword' => $request->isMethod('POST') && $givenPassword !== null,
]);
Expand All @@ -66,7 +66,7 @@ public function viewCustomerProjectAction(#[MapEntity(mapping: ['shareKey' => 's
throw $this->createAccessDeniedException('Requested project does not match customer');
}

if (!$this->viewService->hasAccess($sharedPortal, $givenPassword)) {
if (!$this->viewService->hasAccess($sharedPortal, $givenPassword, $request)) {
return $this->render('@CustomerPortal/view/auth.html.twig', [
'invalidPassword' => $request->isMethod('POST') && $givenPassword !== null,
]);
Expand Down
11 changes: 11 additions & 0 deletions DependencyInjection/CustomerPortalExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,16 @@ public function prepend(ContainerBuilder $container): void
'customer_portal' => 'auto',
],
]);

$container->prependExtensionConfig('framework', [
'rate_limiter' => [
'customer_portal' => [
'policy' => 'fixed_window',
'limit' => 10,
'interval' => '1 hour',
'lock_factory' => null,
],
],
]);
}
}
14 changes: 12 additions & 2 deletions Service/ViewService.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
use KimaiPlugin\CustomerPortalBundle\Model\RecordMergeMode;
use KimaiPlugin\CustomerPortalBundle\Model\TimeRecord;
use KimaiPlugin\CustomerPortalBundle\Repository\SharedProjectTimesheetRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;

class ViewService
{
Expand All @@ -30,14 +32,15 @@ public function __construct(
private readonly RequestStack $request,
private readonly PasswordHasherFactoryInterface $passwordHasherFactory,
private readonly SharedProjectTimesheetRepository $sharedTimesheetRepository,
private readonly RateLimiterFactory $customerPortalLimiter
)
{
}

/**
* Check if the user has access to the given shared project timesheet.
*/
public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenPassword): bool
public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenPassword, Request $request): bool
{
$hashedPassword = $sharedProject->getPassword();

Expand All @@ -47,11 +50,18 @@ public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenP
$sessionPasswordKey = \sprintf('spt-authed-%d-%s', $sharedProject->getId(), $shareKey);

if (!$this->request->getSession()->has($sessionPasswordKey)) {
$limiter = $this->customerPortalLimiter->create($request->getClientIp());
$limit = $limiter->consume();

if (!$limit->isAccepted()) {
return false;
}

// Check given password
if ($givenPassword === null || $givenPassword === '' || !$this->passwordHasherFactory->getPasswordHasher('customer_portal')->verify($hashedPassword, $givenPassword)) {
return false;
}

$limiter->reset();
$this->request->getSession()->set($sessionPasswordKey, true);
}
}
Expand Down

0 comments on commit 923e9d9

Please sign in to comment.