more csrf protection for invoice and search (#2984)

This commit is contained in:
Kevin Papst
2021-12-02 18:01:51 +01:00
committed by GitHub
parent b0045a910c
commit 4e42911f3d
7 changed files with 73 additions and 20 deletions

View File

@@ -207,6 +207,17 @@ abstract class AbstractController extends BaseAbstractController implements Serv
throw new \InvalidArgumentException('handleSearchForm() requires an instanceof BaseQuery as form data'); throw new \InvalidArgumentException('handleSearchForm() requires an instanceof BaseQuery as form data');
} }
$actions = ['resetSearchFilter', 'removeDefaultQuery', 'setDefaultQuery'];
foreach ($actions as $action) {
if ($request->query->has($action)) {
if (!$this->isCsrfTokenValid('search', $request->query->get('_token'))) {
$this->flashError('action.csrf.error');
return false;
}
}
}
if ($request->query->has('resetSearchFilter')) { if ($request->query->has('resetSearchFilter')) {
$data->resetFilter(); $data->resetFilter();
$this->removeLastSearch($data); $this->removeLastSearch($data);

View File

@@ -76,6 +76,13 @@ final class InvoiceController extends AbstractController
} }
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$token = null;
if ($request->query->has('token')) {
$token = $request->query->get('token');
$request->query->remove('token');
}
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form')); $form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
if ($this->handleSearch($form, $request)) { if ($this->handleSearch($form, $request)) {
return $this->redirectToRoute('invoice'); return $this->redirectToRoute('invoice');
@@ -87,6 +94,12 @@ final class InvoiceController extends AbstractController
if ($form->isValid() && $this->isGranted('create_invoice')) { if ($form->isValid() && $this->isGranted('create_invoice')) {
if ($request->query->has('createInvoice')) { if ($request->query->has('createInvoice')) {
if (!$this->isCsrfTokenValid('invoice.create', $token)) {
$this->flashError('action.csrf.error');
return $this->redirectToRoute('invoice');
}
try { try {
return $this->renderInvoice($query, $request); return $this->renderInvoice($query, $request);
} catch (Exception $ex) { } catch (Exception $ex) {
@@ -160,6 +173,18 @@ final class InvoiceController extends AbstractController
return $this->redirectToRoute('invoice'); return $this->redirectToRoute('invoice');
} }
$token = null;
if ($request->query->has('token')) {
$token = $request->query->get('token');
$request->query->remove('token');
}
if (!$this->isCsrfTokenValid('invoice.create', $token)) {
$this->flashError('action.csrf.error');
return $this->redirectToRoute('invoice');
}
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form')); $form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
$form->submit($request->query->all(), false); $form->submit($request->query->all(), false);
@@ -177,14 +202,22 @@ final class InvoiceController extends AbstractController
} }
/** /**
* @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET", "POST"}) * @Route(path="/change-status/{id}/{status}/{token}", name="admin_invoice_status", methods={"GET", "POST"})
* @Security("is_granted('access', invoice.getCustomer())") * @Security("is_granted('access', invoice.getCustomer())")
* @Security("is_granted('create_invoice')") * @Security("is_granted('create_invoice')")
*/ */
public function changeStatusAction(Invoice $invoice, string $status, Request $request): Response public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
$this->flashError('action.csrf.error');
return $this->redirectToRoute('admin_invoice_list');
}
$token = $csrfTokenManager->refreshToken('invoice.status');
if ($status === Invoice::STATUS_PAID) { if ($status === Invoice::STATUS_PAID) {
$form = $this->createPaymentDateForm($invoice, $status); $form = $this->createPaymentDateForm($invoice, $status, $token->getValue());
$form->handleRequest($request); $form->handleRequest($request);
if (!$form->isSubmitted() || !$form->isValid()) { if (!$form->isSubmitted() || !$form->isValid()) {
@@ -212,13 +245,13 @@ final class InvoiceController extends AbstractController
*/ */
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete', $token))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
$this->flashError('action.csrf.error'); $this->flashError('action.csrf.error');
return $this->redirectToRoute('admin_invoice_list'); return $this->redirectToRoute('admin_invoice_list');
} }
$csrfTokenManager->refreshToken('invoice.delete'); $csrfTokenManager->refreshToken('invoice.status');
try { try {
$this->service->deleteInvoice($invoice); $this->service->deleteInvoice($invoice);
@@ -624,13 +657,13 @@ final class InvoiceController extends AbstractController
]); ]);
} }
private function createPaymentDateForm(Invoice $invoice, string $status): FormInterface private function createPaymentDateForm(Invoice $invoice, string $status, string $token): FormInterface
{ {
if (null === $invoice->getPaymentDate()) { if (null === $invoice->getPaymentDate()) {
$invoice->setPaymentDate($this->getDateTimeFactory()->createDateTime()); $invoice->setPaymentDate($this->getDateTimeFactory()->createDateTime());
} }
$url = $this->generateUrl('admin_invoice_status', ['id' => $invoice->getId(), 'status' => $status]); $url = $this->generateUrl('admin_invoice_status', ['id' => $invoice->getId(), 'status' => $status, 'token' => $token]);
return $this->createForm(InvoicePaymentDateForm::class, $invoice, [ return $this->createForm(InvoicePaymentDateForm::class, $invoice, [
'action' => $url, 'action' => $url,

View File

@@ -31,15 +31,15 @@ class InvoiceSubscriber extends AbstractActionsSubscriber
} }
if (!$invoice->isPending()) { if (!$invoice->isPending()) {
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]); $event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending', 'token' => $payload['token']])]);
} else { } else {
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid']), 'class' => 'modal-ajax-form']); $event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid', 'token' => $payload['token']]), 'class' => 'modal-ajax-form']);
} }
$allowDelete = $this->isGranted('delete_invoice'); $allowDelete = $this->isGranted('delete_invoice');
if (!$invoice->isCanceled()) { if (!$invoice->isCanceled()) {
$id = $allowDelete ? 'invoice.cancel' : 'trash'; $id = $allowDelete ? 'invoice.cancel' : 'trash';
$event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled']), 'title' => 'invoice.cancel', 'translation_domain' => 'actions']); $event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled', 'token' => $payload['token']]), 'title' => 'invoice.cancel', 'translation_domain' => 'actions']);
} }
$event->addDivider(); $event->addDivider();

View File

@@ -6,7 +6,7 @@
{% macro invoice(invoice, view) %} {% macro invoice(invoice, view) %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoice', view, {'invoice': invoice, 'token': csrf_token('invoice.delete')}) %} {% set event = actions(app.user, 'invoice', view, {'invoice': invoice, 'token': csrf_token('invoice.status')}) %}
{{ widgets.table_actions(event.actions) }} {{ widgets.table_actions(event.actions) }}
{% endmacro %} {% endmacro %}

View File

@@ -218,7 +218,7 @@
const overwrites = {'customers[]': link.dataset['customer'], 'template': link.dataset['template']}; const overwrites = {'customers[]': link.dataset['customer'], 'template': link.dataset['template']};
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'), overwrites); const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'), overwrites);
link.href = link.dataset['href'] + '?' + uri; link.href = link.dataset['href'] + '?token={{ csrf_token('invoice.create') }}&' + uri;
return true; return true;
} }
@@ -228,7 +228,7 @@
const formPlugin = kimai.getPlugin('form'); const formPlugin = kimai.getPlugin('form');
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}')); const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'));
link.href = '{{ path('invoice') }}?createInvoice=true&' + uri; link.href = '{{ path('invoice') }}?token={{ csrf_token('invoice.create') }}&createInvoice=true&' + uri;
return true; return true;
} }

View File

@@ -40,6 +40,7 @@
{% endmacro %} {% endmacro %}
{% macro searchButton(form) %} {% macro searchButton(form) %}
<input type="hidden" name="_token" value="{{ csrf_token('search') }}">
<div class="btn-group"> <div class="btn-group">
<button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button> <button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
</div> </div>
@@ -56,7 +57,7 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a href="?removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'label.remove_default'|trans }}</a> <a href="?_token={{ csrf_token('search') }}&removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'label.remove_default'|trans }}</a>
</li> </li>
</ul> </ul>
{% else %} {% else %}

View File

@@ -190,7 +190,9 @@ class InvoiceControllerTest extends ControllerBaseTest
'markAsExported' => 1, 'markAsExported' => 1,
]; ];
$action = '/invoice/save-invoice/1/' . $template->getId() . '?' . http_build_query($urlParams); $token = self::$container->get('security.csrf.token_manager')->getToken('invoice.create');
$action = '/invoice/save-invoice/1/' . $template->getId() . '?token=' . $token->getValue() . '&' . http_build_query($urlParams);
$this->request($client, $action); $this->request($client, $action);
$this->assertIsRedirect($client); $this->assertIsRedirect($client);
$this->assertRedirectUrl($client, '/invoice/show?id=', false); $this->assertRedirectUrl($client, '/invoice/show?id=', false);
@@ -286,9 +288,11 @@ class InvoiceControllerTest extends ControllerBaseTest
// but the datatable with all timesheets // but the datatable with all timesheets
$this->assertDataTableRowCount($client, 'datatable_invoice', 20); $this->assertDataTableRowCount($client, 'datatable_invoice', 20);
$token = self::$container->get('security.csrf.token_manager')->getToken('invoice.create');
$form = $client->getCrawler()->filter('#invoice-print-form')->form(); $form = $client->getCrawler()->filter('#invoice-print-form')->form();
$node = $form->getFormNode(); $node = $form->getFormNode();
$node->setAttribute('action', $this->createUrl('/invoice/?createInvoice=true')); $node->setAttribute('action', $this->createUrl('/invoice/?createInvoice=true&token=' . $token->getValue()));
$node->setAttribute('method', 'GET'); $node->setAttribute('method', 'GET');
$client->submit($form, [ $client->submit($form, [
'template' => $template->getId(), 'template' => $template->getId(),
@@ -317,17 +321,20 @@ class InvoiceControllerTest extends ControllerBaseTest
self::assertInstanceOf(BinaryFileResponse::class, $response); self::assertInstanceOf(BinaryFileResponse::class, $response);
self::assertFileExists($response->getFile()); self::assertFileExists($response->getFile());
$this->request($client, '/invoice/change-status/' . $id . '/pending'); $token = self::$container->get('security.csrf.token_manager')->getToken('invoice.status');
$this->request($client, '/invoice/change-status/' . $id . '/pending/' . $token->getValue());
$this->assertIsRedirect($client, '/invoice/show'); $this->assertIsRedirect($client, '/invoice/show');
$client->followRedirect(); $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->request($client, '/invoice/change-status/' . $id . '/paid'); $token = self::$container->get('security.csrf.token_manager')->getToken('invoice.status');
$this->request($client, '/invoice/change-status/' . $id . '/paid/' . $token->getValue());
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$token = self::$container->get('security.csrf.token_manager')->getToken('invoice.status');
$this->assertHasValidationError( $this->assertHasValidationError(
$client, $client,
'/invoice/change-status/' . $id . '/paid', '/invoice/change-status/' . $id . '/paid/' . $token->getValue(),
'form[name=invoice_payment_date_form]', 'form[name=invoice_payment_date_form]',
[ [
'invoice_payment_date_form' => [ 'invoice_payment_date_form' => [
@@ -350,7 +357,8 @@ class InvoiceControllerTest extends ControllerBaseTest
$client->followRedirect(); $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->request($client, '/invoice/change-status/' . $id . '/new'); $token = self::$container->get('security.csrf.token_manager')->getToken('invoice.status');
$this->request($client, '/invoice/change-status/' . $id . '/new/' . $token->getValue());
$this->assertIsRedirect($client, '/invoice/show'); $this->assertIsRedirect($client, '/invoice/show');
$client->followRedirect(); $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());