From 671df1803961336271a44714709998f7090963ae Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Mon, 6 Jul 2026 07:53:11 +0000 Subject: [PATCH] CRITICAL: block direct access to customer license/insurance uploads (was fully downloadable with no auth); fix double-booking race condition and unstable deposit-hold idempotency key --- .htaccess | 7 ++++++ contact.php | 61 ++++++++++++++++++++++++++++++----------------- uploads/.htaccess | 13 ++++------ 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/.htaccess b/.htaccess index 8dde019..29f81da 100644 --- a/.htaccess +++ b/.htaccess @@ -12,3 +12,10 @@ RewriteRule .* - [F,L] # issue found and fixed on sibling sites this session. RewriteCond %{REQUEST_URI} ^/\.git RewriteRule .* - [F,L] + +# uploads/ (customer license/insurance docs) must never be directly downloadable - +# same Order/Deny-doesn't-work-on-OpenLiteSpeed gotcha as .git above. Docs are +# served only through view-doc.php / admin/view-doc.php, which read by filesystem +# path via readfile() - blocking direct URL access here doesn't break that flow. +RewriteCond %{REQUEST_URI} ^/uploads/ +RewriteRule .* - [F,L] diff --git a/contact.php b/contact.php index 8d43b80..8d8c5fb 100644 --- a/contact.php +++ b/contact.php @@ -39,29 +39,43 @@ $pkg = PACKAGES[$package]; $rentalDate = $date; $endDate = date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days')); -// Check availability -$conflict = db()->prepare( - "SELECT id FROM bookings - WHERE status IN ('pending','confirmed') - AND rental_date <= ? AND end_date >= ?" -); -$conflict->execute([$endDate, $rentalDate]); -if ($conflict->fetch()) { - echo json_encode(['success'=>false,'error'=>'Sorry, that date is already booked. Please choose another date.']); exit; -} -$blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?"); -$blockedCheck->execute([$rentalDate, $endDate]); -if ($blockedCheck->fetch()) { - echo json_encode(['success'=>false,'error'=>'That date is unavailable. Please choose another date.']); exit; +// Check availability + create booking atomically. A named advisory lock (scoped to the +// requested date range) prevents two concurrent submissions from both passing the +// conflict check before either has inserted, which would otherwise double-book the +// vehicle for overlapping dates. +$lockName = 'psr_booking_' . $rentalDate . '_' . $endDate; +$gotLock = (int) db()->query("SELECT GET_LOCK('" . addslashes($lockName) . "', 5)")->fetchColumn(); +if (!$gotLock) { + http_response_code(503); + echo json_encode(['success'=>false,'error'=>'Please try again in a moment.']); exit; } -// Create booking -$ref = generateRef(); -$stmt = db()->prepare( - "INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes) - VALUES (?,?,?,?,?,?,?,?,?)" -); -$stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]); +try { + $conflict = db()->prepare( + "SELECT id FROM bookings + WHERE status IN ('pending','confirmed') + AND rental_date <= ? AND end_date >= ?" + ); + $conflict->execute([$endDate, $rentalDate]); + if ($conflict->fetch()) { + echo json_encode(['success'=>false,'error'=>'Sorry, that date is already booked. Please choose another date.']); exit; + } + $blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?"); + $blockedCheck->execute([$rentalDate, $endDate]); + if ($blockedCheck->fetch()) { + echo json_encode(['success'=>false,'error'=>'That date is unavailable. Please choose another date.']); exit; + } + + // Create booking + $ref = generateRef(); + $stmt = db()->prepare( + "INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes) + VALUES (?,?,?,?,?,?,?,?,?)" + ); + $stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]); +} finally { + db()->query("SELECT RELEASE_LOCK('" . addslashes($lockName) . "')"); +} $dateLabel = date('F j, Y', strtotime($rentalDate)); $pkgLabel = $pkg['label']; @@ -127,7 +141,10 @@ $sqId = null; if ($squareToken) { $sqResp = squareApi('POST', '/payments', [ 'source_id' => $squareToken, - 'idempotency_key' => $ref . '-dep-' . time(), + // Stable per booking ref (not time()-suffixed) so a network retry or + // accidental double-submit of this same deposit hold is recognized by + // Square as a duplicate instead of placing a second hold on the card. + 'idempotency_key' => $ref . '-dep', 'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'], 'autocomplete' => false, // hold only — capture when confirmed 'location_id' => SQUARE_LOCATION_ID, diff --git a/uploads/.htaccess b/uploads/.htaccess index 1975525..23a9814 100644 --- a/uploads/.htaccess +++ b/uploads/.htaccess @@ -1,8 +1,5 @@ -# Block direct web access — documents served only through admin/view-doc.php - - Require all denied - - - Order deny,allow - Deny from all - +# Block direct web access - documents served only through view-doc.php / admin/view-doc.php. +# Require all denied / Order deny,allow do NOT work on this OpenLiteSpeed setup - +# only RewriteRule [F,L] actually blocks requests here (confirmed this session). +RewriteEngine On +RewriteRule .* - [F,L]