Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private static function getLevels()
}

$setting = QubitSetting::getByName('inventory_levels');
if (null === $setting || false === $value = unserialize($setting->getValue())) {
if (null === $setting || false === $value = Qubit::safeUnserialize($setting->getValue(), false)) {
return;
}

Expand Down
5 changes: 3 additions & 2 deletions apps/qubit/modules/settings/actions/inventoryAction.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ protected function addField($name)
{
switch ($name) {
case 'levels':
$value = unserialize(
$this->settingLevels->getValue(['sourceCulture' => true])
$value = Qubit::safeUnserialize(
$this->settingLevels->getValue(['sourceCulture' => true]),
false
);

if (false !== $value) {
Expand Down
2 changes: 1 addition & 1 deletion apps/qubit/modules/settings/actions/oaiAction.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class SettingsOaiAction extends sfAction
public function execute($request)
{
// Redirect to global settings form if the OAI plugin is not enabled
if (!in_array('arOaiPlugin', unserialize(sfConfig::get('app_plugins')))) {
if (!in_array('arOaiPlugin', Qubit::safeUnserialize(sfConfig::get('app_plugins'), []))) {
$this->redirect('settings/global');
}

Expand Down
72 changes: 72 additions & 0 deletions lib/Qubit.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,49 @@

class Qubit
{
private const MAX_UNSERIALIZE_INSPECTION_DEPTH = 100;

/**
* Safely unserialize stored application data without rehydrating objects.
*
* @param mixed $value
* @param null|mixed $default
*/
public static function safeUnserialize($value, $default = null)
{
if (!is_string($value) || '' === $value) {
return $default;
}

// Application data only uses serialized arrays/scalars; object
// rehydration is intentionally disabled for stored values.
$unserializeWarning = false;

// Invalid serialized input returns false and raises E_WARNING; valid
// serialized false also returns false, but without raising a warning.
set_error_handler(function () use (&$unserializeWarning) {
$unserializeWarning = true;

return true;
}, E_WARNING);

try {
$data = unserialize($value, ['allowed_classes' => false]);
} finally {
restore_error_handler();
}

if ($unserializeWarning) {
return $default;
}

if (self::containsObject($data)) {
return $default;
}

return $data;
}

public static function pathInfo($url, $request = null)
{
// Allow callers and tests to supply a request explicitly; otherwise use the current one.
Expand Down Expand Up @@ -501,4 +544,33 @@ private static function isSameConfiguredOriginUrl(array $urlParts)

return $urlPort === $baseUrlPort;
}

/**
* Detect object placeholders that remain after class-disabled unserialize.
*
* PHP unserialize() preserves array references, so reject overly deep
* structures before recursive traversal can loop or exhaust memory.
*
* @param mixed $value
*/
private static function containsObject($value, int $depth = 0)
{
if ($depth > self::MAX_UNSERIALIZE_INSPECTION_DEPTH) {
return true;
}

if (is_object($value)) {
return true;
}

if (is_array($value)) {
foreach ($value as $item) {
if (self::containsObject($item, $depth + 1)) {
return true;
}
}
}

return false;
}
}
2 changes: 1 addition & 1 deletion lib/QubitCsvTransform.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public function writeMySQLRowsToCsvFilePath($filepath)
}

// Write CSV row data
$data = unserialize($row['data']);
$data = Qubit::safeUnserialize($row['data'], []);

fputcsv($fhOut, $data);

Expand Down
2 changes: 1 addition & 1 deletion lib/QubitXmlImport.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public function import($xmlFile, $options = [], $xmlOrigFileName = null)
$criteria = new Criteria();
$criteria->add(QubitSetting::NAME, 'plugins');
$setting = QubitSetting::getOne($criteria);
if (null === $setting || !in_array('sfSkosPlugin', unserialize($setting->getValue(['sourceCulture' => true])))) {
if (null === $setting || !in_array('sfSkosPlugin', Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []))) {
throw new sfException($this->i18n->__('The SKOS plugin is not enabled'));
}
$this->rootObject = QubitTaxonomy::getById($options['taxonomy']);
Expand Down
6 changes: 4 additions & 2 deletions lib/filter/QubitSettingsFilter.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ public function execute($filterChain)

// Get settings (from cache if exists)
if ($cache->has($cacheKey)) {
$settings = unserialize($cache->get($cacheKey));
} else {
$settings = Qubit::safeUnserialize($cache->get($cacheKey));
}

if (!isset($settings)) {
$settings = QubitSetting::getSettingsArray();

$cache->set($cacheKey, serialize($settings));
Expand Down
2 changes: 1 addition & 1 deletion lib/form/SettingsPermissionsForm.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected function getPermissionsForm()
throw new sfException('Setting premisAccessRightValues cannot be found');
}

$premisAccessRightValues = unserialize($premisAccessRightValues->getValue(['sourceCulture' => true]));
$premisAccessRightValues = Qubit::safeUnserialize($premisAccessRightValues->getValue(['sourceCulture' => true]), []);
$defaults = QubitSetting::$premisAccessRightValueDefaults;

$form = new sfForm();
Expand Down
2 changes: 1 addition & 1 deletion lib/model/QubitActor.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function __get($name)
}
}

if (isset($this->values[$name]) && null !== $value = unserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
if (isset($this->values[$name]) && null !== $value = Qubit::safeUnserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
return $value;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/model/QubitFunctionObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function __get($name)
}
}

if (isset($this->values[$name]) && null !== $value = unserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
if (isset($this->values[$name]) && null !== $value = Qubit::safeUnserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
return $value;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/model/QubitGrantedRight.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static function getPremisSettings()
*/
$permissions = QubitSetting::getByName('premisAccessRightValues');

return [$act->id, unserialize($permissions->getValue(['sourceCulture' => true]))];
return [$act->id, Qubit::safeUnserialize($permissions->getValue(['sourceCulture' => true]), [])];
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/model/QubitInformationObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function __get($name)
}
}

if (isset($this->values[$name]) && null !== $value = unserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
if (isset($this->values[$name]) && null !== $value = Qubit::safeUnserialize($this->values[$name]->__get('value', $options + ['sourceCulture' => true]))) {
return $value;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/model/QubitMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public function isProtected()
return false;
}

$lockInfo = unserialize(sfConfig::get('app_menu_locking_info', []));
$lockInfo = Qubit::safeUnserialize(sfConfig::get('app_menu_locking_info', []), []);

// If lock info isn't empty and the menu's ID or name indicates it should be locked, then lock it
if (count($lockInfo) && (in_array($this->id, $lockInfo['byId']) || in_array($this->name, $lockInfo['byName']))) {
Expand Down
2 changes: 1 addition & 1 deletion lib/task/tools/atomPluginsTask.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function execute($arguments = [], $options = [])
}

// Array of plugins
$plugins = array_values(unserialize($setting->getValue(['sourceCulture' => true])));
$plugins = array_values(Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []));

if (in_array($arguments['action'], ['add', 'delete']) && !isset($arguments['plugin'])) {
throw new sfException('Missing plugin name.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public static function getPremisData($ioId, $conn)
$statement->execute([$ioId]);

foreach ($statement->fetchAll(PDO::FETCH_OBJ) as $property) {
$value = unserialize($property->value);
$value = Qubit::safeUnserialize($property->value, []);

switch ($property->name) {
case 'fitsAudio':
Expand Down
6 changes: 3 additions & 3 deletions plugins/qbAclPlugin/lib/model/QubitAclPermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function getConstants($options = [])
$value = null;

if (null !== $constants = parent::__get('constants', $options)) {
$value = unserialize($constants);
$value = Qubit::safeUnserialize($constants, []);
}

if (isset($options['name'])) {
Expand All @@ -94,7 +94,7 @@ public function setConstants($value, $options = [])
if (is_array($value)) {
$constants = [];
if (parent::__isset('constants', $options)) {
$constants = unserialize(parent::__get('constants', $options));
$constants = Qubit::safeUnserialize(parent::__get('constants', $options), []);
}

foreach ($value as $key => $val) {
Expand Down Expand Up @@ -124,7 +124,7 @@ public function evaluateConditional($parameters)
return true;
}

$constants = unserialize($this->constants);
$constants = Qubit::safeUnserialize($this->constants, []);

// Substitute constants
if (preg_match_all('/%k\[(\w+)\]/', $conditional, $matches)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
<langusage>
<language langcode="<?php echo strtolower($iso639convertor->getID2($exportLanguage)); ?>"><?php echo format_language($exportLanguage); ?></language>
<?php if (0 < strlen($languageOfDescription = $resource->getPropertyByName('languageOfDescription')->__toString()) && ($authenticated || (1 == sfConfig::get('app_element_visibility_isad_control_languages') && !$findingAid))) { ?>
<?php $langsOfDesc = unserialize($languageOfDescription); ?>
<?php $langsOfDesc = Qubit::safeUnserialize($languageOfDescription, []); ?>
<?php if (is_array($langsOfDesc)) { ?>
<?php foreach ($langsOfDesc as $langcode) { ?>
<?php if ($langcode != $exportLanguage) { ?>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function initialize()
// http://accesstomemory.org/wiki/index.php?title=Autoload
$this->dispatcher->disconnect('autoload.filter_config', [$this->configuration, 'filterAutoloadConfig']);

$pluginNames = unserialize($query[0]->__get('value', ['sourceCulture' => true]));
$pluginNames = Qubit::safeUnserialize($query[0]->__get('value', ['sourceCulture' => true]), []);

// if (isset($_GET['t']))
// {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function execute($request)
if (1 == count($query = QubitSetting::get($criteria))) {
$setting = $query[0];

$this->form->setDefault('enabled', unserialize($setting->getValue(['sourceCulture' => true])));
$this->form->setDefault('enabled', Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []));
}

$configuration = ProjectConfiguration::getActive();
Expand Down Expand Up @@ -77,7 +77,7 @@ public function execute($request)
$setting->name = 'plugins';
}

$settings = unserialize($setting->getValue(['sourceCulture' => true]));
$settings = Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []);

$swordEnabled = in_array('qtSwordPlugin', $settings);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function execute($request)
if (1 == count($query = QubitSetting::get($criteria))) {
$setting = $query[0];

$this->form->setDefault('enabled', unserialize($setting->getValue(['sourceCulture' => true])));
$this->form->setDefault('enabled', Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []));
}

$configuration = ProjectConfiguration::getActive();
Expand Down Expand Up @@ -76,7 +76,7 @@ public function execute($request)
$setting->name = 'plugins';
}

$settings = unserialize($setting->getValue(['sourceCulture' => true]));
$settings = Qubit::safeUnserialize($setting->getValue(['sourceCulture' => true]), []);

foreach (array_keys($this->plugins) as $item) {
if (in_array($item, (array) $this->form->getValue('enabled'))) {
Expand Down
47 changes: 47 additions & 0 deletions test/phpunit/lib/QubitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

use PHPUnit\Framework\TestCase;

/**
* @internal
*
* @covers \Qubit
*/
class QubitTest extends TestCase
{
public function testSafeUnserializeReturnsArraysAndScalars()
{
$this->assertSame(['a' => 1], Qubit::safeUnserialize(serialize(['a' => 1])));
$this->assertSame('value', Qubit::safeUnserialize(serialize('value')));
$this->assertFalse(Qubit::safeUnserialize(serialize(false), true));
}

public function testSafeUnserializeReturnsDefaultForInvalidValues()
{
$this->assertSame([], Qubit::safeUnserialize('not serialized', []));
$this->assertSame([], Qubit::safeUnserialize(null, []));
$this->assertSame([], Qubit::safeUnserialize('', []));
}

public function testSafeUnserializeRejectsObjects()
{
$this->assertSame([], Qubit::safeUnserialize(serialize(new stdClass()), []));
$this->assertSame([], Qubit::safeUnserialize(serialize(['nested' => new stdClass()]), []));
}

public function testSafeUnserializeRejectsRecursiveArrays()
{
$this->assertSame([], Qubit::safeUnserialize('a:1:{i:0;R:1;}', []));
}

public function testSafeUnserializeRejectsDeeplyNestedArrays()
{
$value = 'value';

for ($i = 0; $i < 101; ++$i) {
$value = [$value];
}

$this->assertSame([], Qubit::safeUnserialize(serialize($value), []));
}
}
Loading