| @@ -328,9 +328,27 @@ | ||
| 328 | 328 | // Check booking status |
| 329 | 329 | if (isset($booking->status) && !in_array($booking->status, ['confirmed', 'paid', 'completed'])) { |
| 330 | 330 | return new WP_Error('booking_invalid', __('Booking must be confirmed to access downloads.', 'yatra'), ['status' => 403]); |
| 331 | 331 | } |
| 332 | - | |
| 332 | + | |
| 333 | + // H-2: bind the signed URL to a requester who actually owns this booking. | |
| 334 | + // Without this, any logged-in user could mint a download URL for another | |
| 335 | + // customer's confirmed booking on the same trip. Monitor-first: in monitor | |
| 336 | + // mode this only logs and proceeds (no broken downloads for anyone). | |
| 337 | + if (!$this->requesterOwnsBooking($bookingId, $booking)) { | |
| 338 | + if (\Yatra\Security\Guard::denied('download_url_ownership', [ | |
| 339 | + 'booking_id' => $bookingId, | |
| 340 | + 'download_id' => $downloadId, | |
| 341 | + 'user' => get_current_user_id(), | |
| 342 | + ])) { | |
| 343 | + return new WP_Error( | |
| 344 | + 'forbidden', | |
| 345 | + __('You do not have permission to download this file.', 'yatra'), | |
| 346 | + ['status' => 403] | |
| 347 | + ); | |
| 348 | + } | |
| 349 | + } | |
| 350 | + | |
| 333 | 351 | $requiredBookingId = $bookingId; |
| 334 | 352 | } |
| 335 | 353 | |
| 336 | 354 | // Generate secure download URL |
| @@ -347,8 +365,48 @@ | ||
| 347 | 365 | 'filename' => $filename, |
| 348 | 366 | 'visibility' => $visibility, |
| 349 | 367 | 'title' => $download->title ?? '', |
| 350 | 368 | ], 200); |
| 369 | + } | |
| 370 | + | |
| 371 | + /** | |
| 372 | + * Does the current requester own this booking? (H-2) | |
| 373 | + * | |
| 374 | + * Admins pass; a registered-user booking requires the owning user; a guest | |
| 375 | + * booking (user_id NULL/0) requires the booking-session token bound to it. | |
| 376 | + * Same rule used across the booking-session/payment endpoints. | |
| 377 | + * | |
| 378 | + * @param object|null $booking Booking row, or null when not found. | |
| 379 | + */ | |
| 380 | + private function requesterOwnsBooking(int $bookingId, $booking): bool | |
| 381 | + { | |
| 382 | + if (current_user_can('manage_options')) { | |
| 383 | + return true; | |
| 384 | + } | |
| 385 | + | |
| 386 | + if (!$booking) { | |
| 387 | + return false; | |
| 388 | + } | |
| 389 | + | |
| 390 | + $bookingUserId = (int) ($booking->user_id ?? 0); | |
| 391 | + $currentUserId = (int) get_current_user_id(); | |
| 392 | + | |
| 393 | + if ($bookingUserId > 0) { | |
| 394 | + return $currentUserId === $bookingUserId; | |
| 395 | + } | |
| 396 | + | |
| 397 | + // Guest booking: accept a matching short-lived booking-session token. | |
| 398 | + $token = (isset($_GET['booking_token']) && is_string($_GET['booking_token'])) | |
| 399 | + ? sanitize_text_field((string) wp_unslash($_GET['booking_token'])) | |
| 400 | + : ''; | |
| 401 | + if ($token !== '') { | |
| 402 | + $session = get_transient($token); | |
| 403 | + if (is_array($session) && (int) ($session['booking_id'] ?? 0) === $bookingId) { | |
| 404 | + return true; | |
| 405 | + } | |
| 406 | + } | |
| 407 | + | |
| 408 | + return false; | |
| 351 | 409 | } |
| 352 | 410 | |
| 353 | 411 | /** |
| 354 | 412 | * Check download permission based on visibility |