Commit f9c45a77251 for woocommerce
commit f9c45a7725110ec02c05f078f77dff0cbc82e03d
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Wed Sep 2 12:40:35 2026 -0400
Fix PHP error on malformed download links and correct WPCS annotations (#68265)
diff --git a/plugins/woocommerce/changelog/fix-woo6-114-download-authorization-suppressions b/plugins/woocommerce/changelog/fix-woo6-114-download-authorization-suppressions
new file mode 100644
index 00000000000..8683a38b93d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-114-download-authorization-suppressions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Treat product download links whose query arguments arrive as arrays as invalid links instead of letting them raise a PHP error.
diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php
index 61e3c67d2ef..e2362b021be 100644
--- a/plugins/woocommerce/includes/class-wc-download-handler.php
+++ b/plugins/woocommerce/includes/class-wc-download-handler.php
@@ -34,7 +34,7 @@ class WC_Download_Handler {
* Hook in methods.
*/
public static function init() {
- if ( isset( $_GET['download_file'], $_GET['order'] ) && ( isset( $_GET['email'] ) || isset( $_GET['uid'] ) ) ) { // WPCS: input var ok, CSRF ok.
+ if ( isset( $_GET['download_file'], $_GET['order'] ) && ( isset( $_GET['email'] ) || isset( $_GET['uid'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Emailed download links are bearer URLs; download_product() verifies the order key and email hash.
add_action( 'init', array( __CLASS__, 'download_product' ) );
}
add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
@@ -47,7 +47,15 @@ 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
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Emailed download links are bearer URLs: authorization is the order key plus the billing email or its hash, both verified below, so no nonce can be issued for them.
+
+ // Download links only ever carry scalar values, so reject array input instead of passing it to the string handling below.
+ foreach ( array( 'download_file', 'order', 'key', 'email', 'uid' ) as $download_arg ) {
+ if ( isset( $_GET[ $download_arg ] ) && ! is_scalar( $_GET[ $download_arg ] ) ) {
+ self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
+ }
+ }
+
$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();
@@ -66,16 +74,15 @@ class WC_Download_Handler {
}
// Fallback, accept email address if it's passed.
- if ( empty( $_GET['email'] ) && empty( $_GET['uid'] ) ) { // WPCS: input var ok, CSRF ok.
+ if ( empty( $_GET['email'] ) && empty( $_GET['uid'] ) ) {
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_id = wc_get_order_id_by_order_key( wc_clean( wp_unslash( $_GET['order'] ) ) );
$order = wc_get_order( $order_id );
- if ( isset( $_GET['email'] ) ) { // WPCS: input var ok, CSRF ok.
- $email_address = wp_unslash( $_GET['email'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
+ if ( isset( $_GET['email'] ) ) {
+ $email_address = wp_unslash( $_GET['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The raw value is needed so spaces can be restored to "+" below; sanitize_email() is applied before the lookup.
} else {
// Get email address from order to verify hash.
$email_address = is_a( $order, 'WC_Order' ) ? $order->get_billing_email() : null;
@@ -83,7 +90,7 @@ class WC_Download_Handler {
// Prepare email address hash.
$email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address );
- if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok.
+ if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) {
self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
}
}
@@ -91,15 +98,16 @@ class WC_Download_Handler {
$download_ids = $data_store->get_downloads(
array(
'user_email' => sanitize_email( str_replace( ' ', '+', $email_address ) ),
- 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok.
+ 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ),
'product_id' => $product_id,
- 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok.
+ 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The key is matched against the product's download list above and wc_clean() normalizes it before the lookup.
'orderby' => 'downloads_remaining',
'order' => 'DESC',
'limit' => 1,
'return' => 'ids',
)
);
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( empty( $download_ids ) ) {
self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
diff --git a/plugins/woocommerce/src/Admin/ReportExporter.php b/plugins/woocommerce/src/Admin/ReportExporter.php
index 389b791613e..d2931de08a9 100644
--- a/plugins/woocommerce/src/Admin/ReportExporter.php
+++ b/plugins/woocommerce/src/Admin/ReportExporter.php
@@ -179,11 +179,11 @@ class ReportExporter {
if (
isset( $_GET['action'] ) &&
! empty( $_GET['filename'] ) &&
- self::DOWNLOAD_EXPORT_ACTION === wp_unslash( $_GET['action'] ) && // WPCS: input var ok, sanitization ok.
+ self::DOWNLOAD_EXPORT_ACTION === wp_unslash( $_GET['action'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only export download reached from an emailed admin link and gated on the view_woocommerce_reports capability; the value is only compared verbatim against a fixed action name.
current_user_can( 'view_woocommerce_reports' )
) {
$exporter = new ReportCSVExporter();
- $exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // WPCS: input var ok, sanitization ok.
+ $exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only export download reached from an emailed admin link and gated on the view_woocommerce_reports capability; set_filename() applies sanitize_file_name(), which keeps the read inside the reports directory.
$exporter->export();
}
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
index 5b0f11384af..0992cfe5342 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
@@ -651,6 +651,62 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox download_product() should treat array query args as an invalid download link.
+ */
+ public function test_download_product_rejects_array_query_args(): void {
+ $string_args = array(
+ 'download_file' => '1',
+ 'order' => 'wc_order_x',
+ 'key' => 'k',
+ 'email' => 'a@example.org',
+ );
+
+ $product_was_looked_up = false;
+ $lookup_watcher = function ( $type ) use ( &$product_was_looked_up ) {
+ $product_was_looked_up = true;
+ return $type;
+ };
+
+ add_filter( 'woocommerce_product_type_query', $lookup_watcher );
+
+ try {
+ foreach ( array( 'download_file', 'order', 'key', 'email', 'uid' ) as $arg ) {
+ $_GET = $string_args;
+
+ if ( 'uid' === $arg ) {
+ // The UID is only consulted when no email address is supplied.
+ unset( $_GET['email'] );
+ }
+
+ $_GET[ $arg ] = array( 'x' );
+ $wp_die_message = '';
+ $product_was_looked_up = false;
+
+ // We do not use expectException() here because every argument is checked in turn.
+ try {
+ WC_Download_Handler::download_product();
+ } catch ( WPDieException $e ) {
+ $wp_die_message = $e->getMessage();
+ }
+
+ $this->assertStringContainsString(
+ 'Invalid download link',
+ $wp_die_message,
+ "An array value for the \"$arg\" query argument should render the invalid download link error."
+ );
+
+ $this->assertFalse(
+ $product_was_looked_up,
+ "Array query arguments are rejected before any product lookup, but the \"$arg\" case reached one."
+ );
+ }
+ } finally {
+ remove_filter( 'woocommerce_product_type_query', $lookup_watcher );
+ $_GET = array();
+ }
+ }
+
/**
* Creates a downloadable product, and then places (and completes) an order for that
* object.