more csrf protection for invoice and search (#2984)
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user