Merge pull request #173 from copenhagentruckwash/fix-cross-tenant-certificate-attachment-vulnerability

Validate booking order context before certificates
This commit is contained in:
Jeppe B
2026-06-01 21:00:33 +02:00
committed by GitHub
2 changed files with 47 additions and 2 deletions
@@ -369,6 +369,7 @@ class order_bookings_o extends db
return;
}
$this->requireLinkedOrderMatchesBooking($order);
$normalizedSafetySeal = orders_o::normalizeSafetySealValue($safety_seal);
if ($normalizedSafetySeal !== null) {
$order->setSafetySealValue($normalizedSafetySeal);
@@ -467,14 +468,34 @@ class order_bookings_o extends db
return $order;
}
/**
* @throws Exception
*/
private function requireLinkedOrderMatchesBooking(orders_o $order): void
{
self::requireSelected();
$bookingCustomerNumber = (int)$this->customer_number->value();
$bookingDepartmentId = (int)$this->department->value();
$orderCustomerId = (int)$order->customer_id->value();
$orderDepartmentId = (int)$order->department_id->value();
if ($orderCustomerId !== $bookingCustomerNumber || $orderDepartmentId !== $bookingDepartmentId) {
throw new Exception('Linked order does not match booking customer or department');
}
}
/**
* @throws Exception
*/
protected function attachWashCertificate(int $user_id, ?string $safety_seal = null): void
{
self::requireSelected();
$order = $this->getOrder();
$this->requireLinkedOrderMatchesBooking($order);
// Check if the order already has a wash certificate attached
if ($this->getOrder()->hasWashCertificateAttached()) {
if ($order->hasWashCertificateAttached()) {
return;
}
// Get the operator name
@@ -15,8 +15,12 @@ if (!class_exists('OrderBookingsCompletionOrderDouble')) {
{
$this->id = -1;
$this->booking_id = new object_property('orders', -1, 'booking_id', 'int', false);
$this->customer_id = new object_property('orders', -1, 'customer_id', 'int', true);
$this->department_id = new object_property('orders', -1, 'department_id', 'int', true);
$this->safety_seal = new object_property('orders', -1, 'safety_seal', 'string', false);
$this->completed_at = new object_property('orders', -1, 'completed_at', 'timestamp', false);
$this->customer_id->set(111111);
$this->department_id->set(10);
}
public bool $washCertificateAttached = false;
@@ -52,8 +56,12 @@ if (!class_exists('OrderBookingsCompletionDouble')) {
{
$this->id = -1;
$this->linkedOrder = $linkedOrder;
$this->customer_number = new object_property('order_bookings', -1, 'customer_number', 'int', true);
$this->department = new object_property('order_bookings', -1, 'department', 'int', true);
$this->order_id = new object_property('order_bookings', -1, 'order_id', 'int', false);
$this->items = new object_property('order_bookings', -1, 'items', 'json', false);
$this->customer_number->set(111111);
$this->department->set(10);
$this->items->set([]);
}
@@ -91,7 +99,7 @@ if (!class_exists('OrderBookingsCompletionDouble')) {
}
}
it('attaches and emails a wash certificate when a booking is already linked to a pos order without one', function (): void {
it('attaches and emails a wash certificate when a booking is already linked to a matching pos order without one', function (): void {
$order = new OrderBookingsCompletionOrderDouble();
$booking = new OrderBookingsCompletionDouble($order);
$booking->order_id->set(321);
@@ -104,6 +112,22 @@ it('attaches and emails a wash certificate when a booking is already linked to a
expect($booking->sendCalls)->toBe(1);
});
it('rejects wash certificate completion when the linked pos order belongs to another booking context', function (): void {
$order = new OrderBookingsCompletionOrderDouble();
$order->customer_id->set(222222);
$order->department_id->set(99);
$booking = new OrderBookingsCompletionDouble($order);
$booking->order_id->set(321);
$booking->containsWashCertificate = true;
expect(fn() => $booking->completeBooking(77, 'LINKED-SEAL'))
->toThrow(Exception::class, 'Linked order does not match booking customer or department');
expect($order->getSafetySealValue())->toBeNull();
expect($booking->attachCalls)->toBe(0);
expect($booking->sendCalls)->toBe(0);
});
it('uses the linked pos order wash certificate item added during mobile completion', function (): void {
$order = new OrderBookingsCompletionOrderDouble();
$order->containsWashCertificate = true;