Protect against use of download links, where the directory is no longer approved.

This commit is contained in:
barryhughes 2022-04-25 15:58:55 -07:00
parent 004b007199
commit 99f85a55d5
2 changed files with 107 additions and 1 deletions

View File

@ -31,11 +31,21 @@ class WC_Download_Handler {
* Check if we need to download a file and check validity.
*/
public static function download_product() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$product_id = absint( $_GET['download_file'] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.VIP.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$product = wc_get_product( $product_id );
$downloads = $product ? $product->get_downloads() : array();
$data_store = WC_Data_Store::load( 'customer-download' );
if ( ! $product || empty( $_GET['key'] ) || empty( $_GET['order'] ) ) { // WPCS: input var ok, CSRF ok.
$key = empty( $_GET['key'] ) ? '' : sanitize_text_field( wp_unslash( $_GET['key'] ) );
if (
! $product
|| empty( $key )
|| empty( $_GET['order'] )
|| ! isset( $downloads[ $key ] )
|| ! $downloads[ $key ]->get_enabled()
) {
self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
}
@ -43,6 +53,7 @@ class WC_Download_Handler {
if ( empty( $_GET['email'] ) && empty( $_GET['uid'] ) ) { // WPCS: input var ok, CSRF ok.
self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$order_id = wc_get_order_id_by_order_key( wc_clean( wp_unslash( $_GET['order'] ) ) ); // WPCS: input var ok, CSRF ok.
$order = wc_get_order( $order_id );

View File

@ -1,5 +1,7 @@
<?php
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Approved_Directories;
/**
* Class WC_Download_Handler_Tests.
*/
@ -49,4 +51,97 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {
$parsed_file_path = WC_Download_Handler::parse_file_path( $remote_file_path );
$this->assertTrue( $parsed_file_path['remote_file'] );
}
/**
* @testdox Customers may not use a direct download link to obtain a downloadable file that has been disabled.
*/
public function test_inactive_downloads_will_not_be_served() {
self::remove_download_handlers();
$downloads_served = 0;
$download_counter = function () use ( &$downloads_served ) {
$downloads_served++;
};
// Track downloads served.
add_action( 'woocommerce_download_file_force', $download_counter );
/**
* @var Approved_Directories $approved_directories
*/
$approved_directories = wc_get_container()->get( Approved_Directories::class );
$approved_directories->set_mode( Approved_Directories::MODE_ENABLED );
$approved_directories->add_approved_directory( 'https://always.trusted' );
$approved_directory_rule_id = $approved_directories->add_approved_directory( 'https://new.supplier' );
$product = WC_Helper_Product::create_downloadable_product(
array(
array(
'name' => 'Book 1',
'file' => 'https://always.trusted/123.pdf',
),
array(
'name' => 'Book 2',
'file' => 'https://new.supplier/456.pdf',
),
)
);
$customer = WC_Helper_Customer::create_customer();
$email = 'admin@example.org';
$order = WC_Helper_Order::create_order( $customer->get_id(), $product );
$order->set_status( 'completed' );
$order->save();
$product_id = $product->get_id();
$downloads = $product->get_downloads();
$download_keys = array_keys( $downloads );
// phpcs:disable WordPress.Security.NonceVerification.Recommended WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$_GET = array(
'download_file' => $product_id,
'order' => $order->get_order_key(),
'email' => $email,
'uid' => hash( 'sha256', $email ),
'key' => $download_keys[0],
);
WC_Download_Handler::download_product();
$this->assertEquals( 1, $downloads_served, 'Valid download request (download key 1 - corresponding approved directory rule enabled) was successfully served.' );
$_GET['key'] = $download_keys[1];
WC_Download_Handler::download_product();
$this->assertEquals( 2, $downloads_served, 'Valid download request (download key 2 - corresponding approved directory rule enabled) was successfully served.' );
$approved_directories->disable_by_id( $approved_directory_rule_id );
$this->expectException( WPDieException::class );
WC_Download_Handler::download_product();
$this->assertEquals( 2, $downloads_served, 'Invalid download request (download key 2 - corresponding approved directory rule disabled) was not served.' );
$_GET['key'] = $download_keys[0];
WC_Download_Handler::download_product();
$this->assertEquals( 1, $downloads_served, 'Valid download request (download key 1 - corresponding approved directory rule remained enabled) was still successfully served.' );
// Cleanup.
add_action( 'woocommerce_download_file_force', $download_counter );
self::restore_download_handlers();
}
/**
* Unregister download handlers to prevent unwanted output and side-effects.
*/
private static function remove_download_handlers() {
remove_action( 'woocommerce_download_file_xsendfile', array( WC_Download_Handler::class, 'download_file_xsendfile' ) );
remove_action( 'woocommerce_download_file_redirect', array( WC_Download_Handler::class, 'download_file_redirect' ) );
remove_action( 'woocommerce_download_file_force', array( WC_Download_Handler::class, 'download_file_force' ) );
}
/**
* Restores download handlers in case needed by other tests.
*/
private static function restore_download_handlers() {
add_action( 'woocommerce_download_file_redirect', array( WC_Download_Handler::class, 'download_file_redirect' ), 10, 2 );
add_action( 'woocommerce_download_file_xsendfile', array( WC_Download_Handler::class, 'download_file_xsendfile' ), 10, 2 );
add_action( 'woocommerce_download_file_force', array( WC_Download_Handler::class, 'download_file_force' ), 10, 2 );
}
}