Merge pull request #256 from copenhagentruckwash/fix-idor-vulnerability-in-economic-v2-endpoints

Prevent IDOR on Economic V2 collected-invoice endpoints
This commit is contained in:
Jeppe B
2026-06-01 23:58:41 +02:00
committed by GitHub
2 changed files with 63 additions and 0 deletions
@@ -1816,6 +1816,7 @@ class orderInvoicesRoute
$warnings = [];
$invoice = (new collected_order_invoices_o())->select($collected_invoice_id);
$invoice->requireSelected();
$this->requireCollectedInvoiceContextAccess($invoice);
$draft_id = null;
$booked_id = null;
@@ -1935,6 +1936,56 @@ class orderInvoicesRoute
];
}
private function requireCollectedInvoiceContextAccess(collected_order_invoices_o $invoice): void
{
global $response;
if ($this->hasPermission('superuser')) {
return;
}
$invoice_customer_number = (int)$invoice->customer_number->value();
if ($invoice_customer_number > 0 && $this->isOwnCustomerContext($invoice_customer_number)) {
return;
}
if ($this->hasAccessToAllCollectedInvoiceDepartments((int)$invoice->id)) {
return;
}
$response->error('Permission denied for requested collected invoice.', 403);
}
private function hasAccessToAllCollectedInvoiceDepartments(int $collected_invoice_id): bool
{
$orders = new orders_o();
$order_departments = $orders->getFieldsWhere(
[
'invoice_collection_id' => $collected_invoice_id,
'deleted_at' => null,
],
['department_id']
);
$department_ids = array_values(array_unique(array_filter(array_map(static function (array $order): int {
return (int)($order['department_id'] ?? 0);
}, $order_departments), static function (int $department_id): bool {
return $department_id > 0;
})));
if (empty($department_ids)) {
return false;
}
foreach ($department_ids as $department_id) {
if (!$this->hasDepartmentAccess((string)$department_id)) {
return false;
}
}
return true;
}
private function extractEconomicCustomerNumber(mixed $invoice_raw): ?int
{
if ($invoice_raw === null) {
@@ -77,3 +77,15 @@ it('keeps legacy compare endpoint path for backward compatibility', function ():
expect($content)->toContain('/collected-invoices/economic/compare');
expect($content)->toContain("requirePermission('compare_collected_invoice_economic')");
});
it('enforces customer or department context before building v2 invoice details payload', function (): void {
$routeFile = app_path('routes/orderInvoicesRoute.php');
$content = file_get_contents($routeFile);
expect($content)->not->toBeFalse();
expect($content)->toContain('private function requireCollectedInvoiceContextAccess');
expect($content)->toContain('$this->requireCollectedInvoiceContextAccess($invoice);');
expect($content)->toContain("$this->hasPermission('superuser')");
expect($content)->toContain('$this->isOwnCustomerContext($invoice_customer_number)');
expect($content)->toContain('$this->hasAccessToAllCollectedInvoiceDepartments((int)$invoice->id)');
});