| @@ -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 |
| @@ -350,8 +368,48 @@ | ||
| 350 | 368 | ], 200); |
| 351 | 369 | } |
| 352 | 370 | |
| 353 | 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; | |
| 409 | + } | |
| 410 | + | |
| 411 | + /** | |
| 354 | 412 | * Check download permission based on visibility |
| 355 | 413 | */ |
| 356 | 414 | public function check_download_permission(WP_REST_Request $request): bool |
| 357 | 415 | { |
| @@ -508,9 +566,8 @@ | ||
| 508 | 566 | $attachmentId = (int) $download->attachment_id; |
| 509 | 567 | } |
| 510 | 568 | |
| 511 | 569 | if ($attachmentId <= 0) { |
| 512 | - error_log("ensureProtectedFile: No attachment_id found for download " . ($download->id ?? 'unknown')); | |
| 513 | 570 | return ''; |
| 514 | 571 | } |
| 515 | 572 | |
| 516 | 573 | $source = get_attached_file($attachmentId); |