Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/download-monitor/download-monitor.php
+++ b/download-monitor/download-monitor.php
@@ -3,7 +3,7 @@
Plugin Name: Download Monitor
Plugin URI: https://www.download-monitor.com
Description: A full solution for managing and selling downloadable files, monitoring downloads and outputting download links and file information on your WordPress powered site.
- Version: 5.1.7
+ Version: 5.1.8
Author: WPChill
Author URI: https://wpchill.com
Requires at least: 6.4
@@ -34,7 +34,7 @@
} // Exit if accessed directly
// Define DLM Version
-define('DLM_VERSION', '5.1.7');
+define('DLM_VERSION', '5.1.8');
define('DLM_UPGRADER_VERSION', '4.6.0');
// Define DLM FILE
--- a/download-monitor/src/Dependencies/Pimple/Container.php
+++ b/download-monitor/src/Dependencies/Pimple/Container.php
@@ -196,7 +196,7 @@
throw new ExpectedInvokableException('Callable is not a Closure or invokable object.');
}
- $this->protected->attach($callable);
+ $this->protected->offsetSet($callable);
return $callable;
}
@@ -268,8 +268,8 @@
};
if (isset($this->factories[$factory])) {
- $this->factories->detach($factory);
- $this->factories->attach($extended);
+ $this->factories->offsetUnset($factory);
+ $this->factories->offsetSet($extended);
}
return $this[$id] = $extended;
--- a/download-monitor/src/Dependencies/Pimple/Tests/PimpleTest.php
+++ b/download-monitor/src/Dependencies/Pimple/Tests/PimpleTest.php
@@ -29,6 +29,7 @@
use PHPUnitFrameworkAttributesDataProvider;
use PHPUnitFrameworkTestCase;
use WPChillDownloadMonitorDependenciesPimpleContainer;
+use WPChillDownloadMonitorDependenciesPimpleServiceProviderInterface;
/**
* @author Igor Wiedler <igor@wiedler.ch>
@@ -204,7 +205,14 @@
public function testFluentRegister()
{
$pimple = new Container();
- $this->assertSame($pimple, $pimple->register($this->getMockBuilder('WPChillDownloadMonitorDependenciesPimpleServiceProviderInterface')->getMock()));
+
+ $stub = new class implements ServiceProviderInterface {
+ public function register(Container $pimple)
+ {
+ }
+ };
+
+ $this->assertSame($pimple, $pimple->register($stub));
}
public function testRawValidatesKeyIsPresent()
@@ -275,13 +283,13 @@
unset($pimple['foo']);
$p = new ReflectionProperty($pimple, 'values');
- if (PHP_VERSION < 80100) {
+ if (PHP_VERSION_ID < 80100) {
$p->setAccessible(true);
}
$this->assertEmpty($p->getValue($pimple));
$p = new ReflectionProperty($pimple, 'factories');
- if (PHP_VERSION < 80100) {
+ if (PHP_VERSION_ID < 80100) {
$p->setAccessible(true);
}
$this->assertCount(0, $p->getValue($pimple));
@@ -425,6 +433,7 @@
/**
* @group legacy
* @expectedDeprecation How WPChillDownloadMonitorDependenciesPimple behaves when extending protected closures will be fixed in WPChillDownloadMonitorDependenciesPimple 4. Are you sure "foo" should be protected?
+ * @dataProvider badServiceDefinitionProvider
*/
#[DataProvider('badServiceDefinitionProvider')]
public function testExtendingProtectedClosureDeprecation($service)
--- a/download-monitor/src/Shop/Checkout/PaymentGateway/PayPal/CaptureOrder.php
+++ b/download-monitor/src/Shop/Checkout/PaymentGateway/PayPal/CaptureOrder.php
@@ -26,6 +26,15 @@
$this->response = $response;
}
+ /**
+ * Whether a response was set (e.g. capture succeeded).
+ *
+ * @return bool
+ */
+ public function has_response() {
+ return isset( $this->response->result );
+ }
+
public function getStatus() {
return $this->response->result->status;
}
@@ -50,7 +59,7 @@
public function captureOrder() {
try {
- $request = new OrdersCaptureRequest( $this->order_id );
+ $request = new OrdersCaptureRequest( $this->order_id );
$request->body = self::buildRequestBody();
$response = $this->client->execute( $request );
@@ -59,8 +68,8 @@
} catch ( PayPalHttpHttpException $ex ) {
- //print_r($ex->getMessage());
-
+ // Capture failed (e.g. invalid token, already captured, network error).
+ return null;
}
}
--- a/download-monitor/src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php
+++ b/download-monitor/src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php
@@ -38,6 +38,7 @@
if ( empty( $order_id ) || empty( $order_hash ) ) {
$this->execute_failed( $order_id, $order_hash );
+ return;
}
/** @var WPChillDownloadMonitorShopOrderRepository $order_repo */
@@ -53,16 +54,41 @@
return;
}
+ // Verify order_hash against the retrieved order (timing-safe) to prevent IDOR.
+ if ( ! hash_equals( (string) $order->get_hash(), (string) $order_hash ) ) {
+ $this->execute_failed( $order_id, $order_hash );
+ return;
+ }
+
/**
- * Get payment identifier
+ * Get payment identifier (PayPal order ID / token)
*/
$token = '';
if ( isset( $_GET['token'] ) ) {
$token = sanitize_text_field( wp_unslash( $_GET['token'] ) );
}
+ if ( empty( $token ) ) {
+ $this->execute_failed( $order_id, $order_hash );
+ return;
+ }
+
+ // Bind token to this order: token must match one of this order's transactions (PayPal order ID).
+ $transactions = $order->get_transactions();
+ $token_belongs_to_order = false;
+ foreach ( $transactions as $transaction ) {
+ if ( hash_equals( (string) $transaction->get_processor_transaction_id(), (string) $token ) ) {
+ $token_belongs_to_order = true;
+ break;
+ }
+ }
+ if ( ! $token_belongs_to_order ) {
+ $this->execute_failed( $order_id, $order_hash );
+ return;
+ }
+
/**
- * Execute the payement
+ * Execute the payment
*/
try {
@@ -70,7 +96,15 @@
$capture->set_client( $this->gateway->get_api_context() )
->set_order_id( $token );
- $response = $capture->captureOrder();
+ $capture_result = $capture->captureOrder();
+
+ // Handle capture failures safely (e.g. network error, invalid token).
+ if ( null === $capture_result || ! $capture_result->has_response() ) {
+ $this->execute_failed( $order->get_id(), $order->get_hash() );
+ return;
+ }
+
+ $response = $capture_result;
// if payment is not approved, exit;
if ( $response->getStatus() !== "COMPLETED" ) {
@@ -80,11 +114,11 @@
/**
* Update transaction in local database
*/
-
- // update the order status to 'completed'
- $transactions = $order->get_transactions();
+ // Update the transaction that belongs to this token (already validated above).
+ $transaction_updated = false;
+ $transactions = $order->get_transactions();
foreach ( $transactions as $transaction ) {
- if ( $transaction->get_processor_transaction_id() == $response->getId() ) {
+ if ( hash_equals( (string) $transaction->get_processor_transaction_id(), (string) $token ) ) {
$transaction->set_status( Services::get()->service( 'order_transaction_factory' )->make_status( 'success' ) );
$transaction->set_processor_status( $response->getStatus() );
@@ -95,9 +129,15 @@
}
$order->set_transactions( $transactions );
+ $transaction_updated = true;
break;
}
+ }
+ // Only complete the order if we actually updated a matching transaction (prevents token/amount mismatch).
+ if ( ! $transaction_updated ) {
+ $this->execute_failed( $order->get_id(), $order->get_hash() );
+ return;
}
// set order as completed, this also persists the order
--- a/download-monitor/vendor/composer/installed.php
+++ b/download-monitor/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'wpchill/download-monitor',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '78419fc33f1ec56ec0435c82c42a820686882568',
+ 'reference' => '589dba41f5f9f23158b08854cdc37d1ab7884249',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'wpchill/download-monitor' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '78419fc33f1ec56ec0435c82c42a820686882568',
+ 'reference' => '589dba41f5f9f23158b08854cdc37d1ab7884249',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),