Skip to content

Commit 33b7bc1

Browse files
authored
Merge pull request #112 from deliciousbrains/improve-unserialization-security
Background processing class now accepts $allowed_classes in constructor
2 parents 93ba778 + 1737b47 commit 33b7bc1

File tree

5 files changed

+163
-4
lines changed

5 files changed

+163
-4
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,64 @@ foreach ( $items as $item ) {
178178
}
179179
```
180180

181+
An item can be any valid PHP value, string, integer, array or object. If needed, the $item is serialized when written to the database.
182+
181183
Save and dispatch the queue:
182184

183185
```php
184186
$this->example_process->save()->dispatch();
185187
```
186188

189+
#### Handling serialized objects in queue items
190+
191+
Queue items that contain non-scalar values are serialized when stored in the database. To avoid potential security issues during unserialize, this library provides the option to set the `allowed_classes` option when calling `unserialize()` which limits which classes can be instantiated. It's kept internally as the protected `$allowed_batch_data_classes` property.
192+
193+
To maintain backward compatibility the default value is `true`, meaning that any serialized object will be instantiated. Please note that this default behavior may change in a future major release.
194+
195+
We encourage all users of this library to take advantage of setting a strict value for `$allowed_batch_data_classes`. If possible, set the value to `false` to disallow any objects from being instantiated, or a very limited list of class names, see examples below.
196+
197+
Objects in the serialized string that are not allowed to be instantiated will instead get the class type `__PHP_Incomplete_Class`.
198+
199+
##### Overriding the default `$allowed_batch_data_classes`
200+
201+
The default behavior can be overridden by passing an array of allowed classes to the constructor:
202+
203+
``` php
204+
$allowed_batch_data_classes = array( MyCustomItem::class, MyItemHelper::class );
205+
$this->example_process = new WP_Example_Process( $allowed_batch_data_classes );
206+
```
207+
208+
Or, set the value to `false`:
209+
210+
``` php
211+
$this->example_process = new WP_Example_Process( false );
212+
```
213+
214+
215+
Another way to change the default is to override the `$allowed_batch_data_classes` property in your process class:
216+
217+
``` php
218+
class WP_Example_Process extends WP_Background_Process {
219+
220+
/**
221+
* @var string
222+
*/
223+
protected $prefix = 'my_plugin';
224+
225+
/**
226+
* @var string
227+
*/
228+
protected $action = 'example_process';
229+
230+
/**
231+
*
232+
* @var bool|array
233+
*/
234+
protected $allowed_batch_data_classes = array( MyCustomItem::class, MyItemHelper::class );
235+
...
236+
237+
```
238+
187239
#### Background Process Status
188240

189241
A background process can be queued, processing, paused, cancelled, or none of the above (not started or has completed).

classes/wp-background-process.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ abstract class WP_Background_Process extends WP_Async_Request {
4949
*/
5050
protected $cron_interval_identifier;
5151

52+
/**
53+
* Restrict object instantiation when using unserialize.
54+
*
55+
* @var bool|array
56+
*/
57+
protected $allowed_batch_data_classes = true;
58+
5259
/**
5360
* The status set when process is cancelling.
5461
*
@@ -65,10 +72,26 @@ abstract class WP_Background_Process extends WP_Async_Request {
6572

6673
/**
6774
* Initiate new background process.
75+
*
76+
* @param bool|array $allowed_batch_data_classes Optional. Array of class names that can be unserialized. Default true (any class).
6877
*/
69-
public function __construct() {
78+
public function __construct( $allowed_batch_data_classes = true ) {
7079
parent::__construct();
7180

81+
if ( empty( $allowed_batch_data_classes ) && false !== $allowed_batch_data_classes ) {
82+
$allowed_batch_data_classes = true;
83+
}
84+
85+
if ( ! is_bool( $allowed_batch_data_classes ) && ! is_array( $allowed_batch_data_classes ) ) {
86+
$allowed_batch_data_classes = true;
87+
}
88+
89+
// If allowed_batch_data_classes property set in subclass,
90+
// only apply override if not allowing any class.
91+
if ( true === $this->allowed_batch_data_classes || true !== $allowed_batch_data_classes ) {
92+
$this->allowed_batch_data_classes = $allowed_batch_data_classes;
93+
}
94+
7295
$this->cron_hook_identifier = $this->identifier . '_cron';
7396
$this->cron_interval_identifier = $this->identifier . '_cron_interval';
7497

@@ -459,11 +482,13 @@ public function get_batches( $limit = 0 ) {
459482
$batches = array();
460483

461484
if ( ! empty( $items ) ) {
485+
$allowed_classes = $this->allowed_batch_data_classes;
486+
462487
$batches = array_map(
463-
static function ( $item ) use ( $column, $value_column ) {
488+
static function ( $item ) use ( $column, $value_column, $allowed_classes ) {
464489
$batch = new stdClass();
465490
$batch->key = $item->{$column};
466-
$batch->data = maybe_unserialize( $item->{$value_column} );
491+
$batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes );
467492

468493
return $batch;
469494
},
@@ -722,4 +747,25 @@ public function cancel_process() {
722747
* @return mixed
723748
*/
724749
abstract protected function task( $item );
750+
751+
/**
752+
* Maybe unserialize data, but not if an object.
753+
*
754+
* @param mixed $data Data to be unserialized.
755+
* @param bool|array $allowed_classes Array of class names that can be unserialized.
756+
*
757+
* @return mixed
758+
*/
759+
protected static function maybe_unserialize( $data, $allowed_classes ) {
760+
if ( is_serialized( $data ) ) {
761+
$options = array();
762+
if ( is_bool( $allowed_classes ) || is_array( $allowed_classes ) ) {
763+
$options['allowed_classes'] = $allowed_classes;
764+
}
765+
766+
return @unserialize( $data, $options ); // @phpcs:ignore
767+
}
768+
769+
return $data;
770+
}
725771
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "WP Background Processing can be used to fire off non-blocking asynchronous requests or as a background processing tool, allowing you to queue tasks.",
44
"type": "library",
55
"require": {
6-
"php": ">=5.6"
6+
"php": ">=7.0"
77
},
88
"suggest": {
99
"coenjacobs/mozart": "Easily wrap this library with your own prefix, to prevent collisions when multiple plugins use this library"

tests/Test_WP_Background_Process.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* @package WP-Background-Processing
66
*/
77

8+
require_once __DIR__ . '/fixtures/Test_Batch_Data.php';
9+
810
use PHPUnit\Framework\MockObject\MockObject;
911

1012
/**
@@ -51,6 +53,25 @@ private function getWPBPProperty( string $name ) {
5153
return $property->getValue( $this->wpbp );
5254
}
5355

56+
/**
57+
* Set a property value on WPBP regardless of accessibility.
58+
*
59+
* @param string $name
60+
* @param mixed $value
61+
*
62+
* @return mixed
63+
*/
64+
private function setWPBPProperty( string $name, $value ) {
65+
try {
66+
$property = new ReflectionProperty( 'WP_Background_Process', $name );
67+
} catch ( Exception $e ) {
68+
return new WP_Error( $e->getCode(), $e->getMessage() );
69+
}
70+
$property->setAccessible( true );
71+
72+
return $property->setValue( $this->wpbp, $value );
73+
}
74+
5475
/**
5576
* Execute a method of WPBP regardless of accessibility.
5677
*
@@ -178,6 +199,41 @@ public function test_get_batch() {
178199
$this->assertInstanceOf( 'stdClass', $second_batch );
179200
$this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' );
180201
$this->assertEquals( array( 'more wibble' ), $second_batch->data );
202+
203+
// Tests using a custom class for the $item.
204+
$this->wpbp->delete( $second_batch->key );
205+
$batch_data_object = new Test_Batch_Data();
206+
$this->wpbp->push_to_queue( $batch_data_object );
207+
$this->assertNotEmpty( $this->getWPBPProperty( 'data' ) );
208+
$this->assertEquals( array( $batch_data_object ), $this->getWPBPProperty( 'data' ) );
209+
$this->wpbp->save();
210+
$third_batch = $this->executeWPBPMethod( 'get_batch' );
211+
$this->assertCount( 1, $third_batch->data );
212+
$this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] );
213+
214+
// Explicitly set allowed classes to Test_Batch_Data.
215+
$this->setWPBPProperty( 'allowed_batch_data_classes', array( Test_Batch_Data::class ) );
216+
$third_batch = $this->executeWPBPMethod( 'get_batch' );
217+
$this->assertCount( 1, $third_batch->data );
218+
$this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] );
219+
220+
// Allow a different class.
221+
$this->setWPBPProperty( 'allowed_batch_data_classes', array( stdClass::class ) );
222+
$third_batch = $this->executeWPBPMethod( 'get_batch' );
223+
$this->assertCount( 1, $third_batch->data );
224+
$this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] );
225+
226+
// Disallow all classes.
227+
$this->setWPBPProperty( 'allowed_batch_data_classes', false );
228+
$third_batch = $this->executeWPBPMethod( 'get_batch' );
229+
$this->assertCount( 1, $third_batch->data );
230+
$this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] );
231+
232+
// Allow everything.
233+
$this->setWPBPProperty( 'allowed_batch_data_classes', true );
234+
$third_batch = $this->executeWPBPMethod( 'get_batch' );
235+
$this->assertCount( 1, $third_batch->data );
236+
$this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] );
181237
}
182238

183239
/**

tests/fixtures/Test_Batch_Data.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
class Test_Batch_Data {
4+
public $prop = "value";
5+
}

0 commit comments

Comments
 (0)