mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
[SS-2018-021] Fix potential SQL vulnerability in non-scalar value hyrdation
This commit is contained in:
parent
1aa98e77d3
commit
95505db7d6
@ -1307,6 +1307,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* @param string $baseTable Base table
|
* @param string $baseTable Base table
|
||||||
* @param string $now Timestamp to use for the current time
|
* @param string $now Timestamp to use for the current time
|
||||||
* @param bool $isNewRecord If this is a new record
|
* @param bool $isNewRecord If this is a new record
|
||||||
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
protected function writeManipulation($baseTable, $now, $isNewRecord)
|
protected function writeManipulation($baseTable, $now, $isNewRecord)
|
||||||
{
|
{
|
||||||
@ -1325,6 +1326,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$manipulation[$baseTable]['command'] = 'update';
|
$manipulation[$baseTable]['command'] = 'update';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure none of our field assignment are arrays
|
||||||
|
foreach ($manipulation as $tableManipulation) {
|
||||||
|
if (!isset($tableManipulation['fields'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($tableManipulation['fields'] as $fieldValue) {
|
||||||
|
if (is_array($fieldValue)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'DataObject::writeManipulation: parameterised field assignments are disallowed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the manipulation
|
// Perform the manipulation
|
||||||
DB::manipulate($manipulation);
|
DB::manipulate($manipulation);
|
||||||
}
|
}
|
||||||
@ -2403,6 +2418,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
|
throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($val) && !is_scalar($val)) {
|
||||||
|
$dbField = $this->dbObject($fieldName);
|
||||||
|
if ($dbField && $dbField->scalarValueOnly()) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
'DataObject::setField: %s only accepts scalars',
|
||||||
|
$fieldName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if a field is not existing or has strictly changed
|
// if a field is not existing or has strictly changed
|
||||||
if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
|
if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
|
||||||
// TODO Add check for php-level defaults which are not set in the db
|
// TODO Add check for php-level defaults which are not set in the db
|
||||||
|
@ -309,4 +309,9 @@ abstract class DBComposite extends DBField
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scalarValueOnly()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,18 +335,16 @@ abstract class DBField extends ViewableData implements DBIndexable
|
|||||||
* will be escaped automatically by the prepared query processor, so it
|
* will be escaped automatically by the prepared query processor, so it
|
||||||
* should not be escaped or quoted at all.
|
* should not be escaped or quoted at all.
|
||||||
*
|
*
|
||||||
* The field values could also be in paramaterised format, such as
|
|
||||||
* array('MAX(?,?)' => array(42, 69)), allowing the use of raw SQL values such as
|
|
||||||
* array('NOW()' => array()).
|
|
||||||
*
|
|
||||||
* @see SQLWriteExpression::addAssignments for syntax examples
|
|
||||||
*
|
|
||||||
* @param $value mixed The value to check
|
* @param $value mixed The value to check
|
||||||
* @return mixed The raw value, or escaped parameterised details
|
* @return mixed The raw value, or escaped parameterised details
|
||||||
*/
|
*/
|
||||||
public function prepValueForDB($value)
|
public function prepValueForDB($value)
|
||||||
{
|
{
|
||||||
if ($value === null || $value === "" || $value === false) {
|
if ($value === null ||
|
||||||
|
$value === "" ||
|
||||||
|
$value === false ||
|
||||||
|
($this->scalarValueOnly() && !is_scalar($value))
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return $value;
|
return $value;
|
||||||
@ -653,4 +651,15 @@ DBG;
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this DBField only accepts scalar values.
|
||||||
|
*
|
||||||
|
* Composite DBFields can override this method and return `false` so they can accept arrays of values.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function scalarValueOnly()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ abstract class DBString extends DBField
|
|||||||
public function prepValueForDB($value)
|
public function prepValueForDB($value)
|
||||||
{
|
{
|
||||||
// Cast non-empty value
|
// Cast non-empty value
|
||||||
if (strlen($value)) {
|
if (is_scalar($value) && strlen($value)) {
|
||||||
return (string)$value;
|
return (string)$value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +295,20 @@ class ManyManyList extends RelationList
|
|||||||
$manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID;
|
$manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID;
|
||||||
$manipulation[$this->joinTable]['fields'][$this->foreignKey] = $foreignID;
|
$manipulation[$this->joinTable]['fields'][$this->foreignKey] = $foreignID;
|
||||||
|
|
||||||
|
// Make sure none of our field assignments are arrays
|
||||||
|
foreach ($manipulation as $tableManipulation) {
|
||||||
|
if (!isset($tableManipulation['fields'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($tableManipulation['fields'] as $fieldValue) {
|
||||||
|
if (is_array($fieldValue)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'ManyManyList::add: parameterised field assignments are disallowed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DB::manipulate($manipulation);
|
DB::manipulate($manipulation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,32 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\Tests;
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\Assets\Image;
|
||||||
use SilverStripe\ORM\FieldType\DBBigInt;
|
use SilverStripe\ORM\FieldType\DBBigInt;
|
||||||
use SilverStripe\ORM\FieldType\DBBoolean;
|
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||||
|
use SilverStripe\ORM\FieldType\DBCurrency;
|
||||||
|
use SilverStripe\ORM\FieldType\DBDate;
|
||||||
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||||
use SilverStripe\ORM\FieldType\DBDecimal;
|
use SilverStripe\ORM\FieldType\DBDecimal;
|
||||||
use SilverStripe\ORM\FieldType\DBDouble;
|
use SilverStripe\ORM\FieldType\DBDouble;
|
||||||
|
use SilverStripe\ORM\FieldType\DBEnum;
|
||||||
use SilverStripe\ORM\FieldType\DBFloat;
|
use SilverStripe\ORM\FieldType\DBFloat;
|
||||||
|
use SilverStripe\ORM\FieldType\DBForeignKey;
|
||||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||||
|
use SilverStripe\ORM\FieldType\DBHTMLVarchar;
|
||||||
|
use SilverStripe\ORM\FieldType\DBInt;
|
||||||
|
use SilverStripe\ORM\FieldType\DBLocale;
|
||||||
|
use SilverStripe\ORM\FieldType\DBMoney;
|
||||||
|
use SilverStripe\ORM\FieldType\DBMultiEnum;
|
||||||
|
use SilverStripe\ORM\FieldType\DBPercentage;
|
||||||
|
use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey;
|
||||||
|
use SilverStripe\ORM\FieldType\DBPrimaryKey;
|
||||||
use SilverStripe\ORM\FieldType\DBString;
|
use SilverStripe\ORM\FieldType\DBString;
|
||||||
use SilverStripe\ORM\FieldType\DBTime;
|
use SilverStripe\ORM\FieldType\DBTime;
|
||||||
use SilverStripe\ORM\FieldType\DBVarchar;
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
use SilverStripe\ORM\FieldType\DBText;
|
use SilverStripe\ORM\FieldType\DBText;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\ORM\FieldType\DBYear;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for DBField objects.
|
* Tests for DBField objects.
|
||||||
@ -189,6 +204,54 @@ class DBFieldTest extends SapphireTest
|
|||||||
$this->assertEquals(PHP_INT_MAX, $bigInt->getValue());
|
$this->assertEquals(PHP_INT_MAX, $bigInt->getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderPrepValueForDBArrayValue
|
||||||
|
*/
|
||||||
|
public function testPrepValueForDBArrayValue($dbFieldName, $scalarValueOnly, $extraArgs = [])
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($dbFieldName);
|
||||||
|
/**
|
||||||
|
* @var DBField
|
||||||
|
*/
|
||||||
|
$dbField = $reflection->newInstanceArgs($extraArgs);
|
||||||
|
$dbField->setName('SomeField');
|
||||||
|
$payload = ['GREATEST(0,?)' => '2'];
|
||||||
|
$preparedValue = $dbField->prepValueForDB($payload);
|
||||||
|
$this->assertTrue(
|
||||||
|
!$scalarValueOnly || !is_array($preparedValue),
|
||||||
|
'`prepValueForDB` can not return an array if scalarValueOnly is true'
|
||||||
|
);
|
||||||
|
$this->assertEquals($scalarValueOnly, $dbField->scalarValueOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataProviderPrepValueForDBArrayValue()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[DBBigInt::class, true],
|
||||||
|
[DBBoolean::class, true],
|
||||||
|
[DBCurrency::class, true],
|
||||||
|
[DBDate::class, true],
|
||||||
|
[DBDatetime::class, true],
|
||||||
|
[DBDecimal::class, true],
|
||||||
|
[DBDouble::class, true],
|
||||||
|
[DBEnum::class, true],
|
||||||
|
[DBFloat::class, true],
|
||||||
|
[DBForeignKey::class, true, ['SomeField']],
|
||||||
|
[DBHTMLText::class, true],
|
||||||
|
[DBHTMLVarchar::class, true],
|
||||||
|
[DBInt::class, true],
|
||||||
|
[DBLocale::class, true],
|
||||||
|
[DBMoney::class, false],
|
||||||
|
[DBMultiEnum::class, true, ['SomeField', ['One', 'Two', 'Three']]],
|
||||||
|
[DBPercentage::class, true],
|
||||||
|
[DBPolymorphicForeignKey::class, false, ['SomeField']],
|
||||||
|
[DBText::class, true],
|
||||||
|
[DBTime::class, true],
|
||||||
|
[DBVarchar::class, true],
|
||||||
|
[DBYear::class, true],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testExists()
|
public function testExists()
|
||||||
{
|
{
|
||||||
$varcharField = new DBVarchar("testfield");
|
$varcharField = new DBVarchar("testfield");
|
||||||
|
@ -16,7 +16,9 @@ use SilverStripe\ORM\FieldType\DBField;
|
|||||||
use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey;
|
use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey;
|
||||||
use SilverStripe\ORM\FieldType\DBVarchar;
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
use SilverStripe\ORM\ManyManyList;
|
use SilverStripe\ORM\ManyManyList;
|
||||||
|
use SilverStripe\ORM\Tests\DataObjectTest\Company;
|
||||||
use SilverStripe\ORM\Tests\DataObjectTest\Player;
|
use SilverStripe\ORM\Tests\DataObjectTest\Player;
|
||||||
|
use SilverStripe\ORM\Tests\DataObjectTest\Team;
|
||||||
use SilverStripe\View\ViewableData;
|
use SilverStripe\View\ViewableData;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@ -2129,4 +2131,20 @@ class DataObjectTest extends SapphireTest
|
|||||||
['"DataObjectTest_TeamComment"."Name"' => 'does not exists']
|
['"DataObjectTest_TeamComment"."Name"' => 'does not exists']
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSetFieldWithArrayOnScalarOnlyField()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$do = Company::singleton();
|
||||||
|
$do->FoundationYear = '1984';
|
||||||
|
$do->FoundationYear = array('Amount' => 123, 'Currency' => 'CAD');
|
||||||
|
$this->assertEmpty($do->FoundationYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetFieldWithArrayOnCompositeField()
|
||||||
|
{
|
||||||
|
$do = Company::singleton();
|
||||||
|
$do->SalaryCap = array('Amount' => 123456, 'Currency' => 'CAD');
|
||||||
|
$this->assertNotEmpty($do->SalaryCap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ class Company extends DataObject implements TestOnly
|
|||||||
private static $table_name = 'DataObjectTest_Company';
|
private static $table_name = 'DataObjectTest_Company';
|
||||||
|
|
||||||
private static $db = [
|
private static $db = [
|
||||||
'Name' => 'Varchar'
|
'Name' => 'Varchar',
|
||||||
|
'MarketValue' => 'Money',
|
||||||
|
'FoundationYear' => 'Year'
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $has_one = [
|
private static $has_one = [
|
||||||
|
@ -10,6 +10,8 @@ use SilverStripe\ORM\ManyManyList;
|
|||||||
/**
|
/**
|
||||||
* @property string Title
|
* @property string Title
|
||||||
* @property string DatabaseField
|
* @property string DatabaseField
|
||||||
|
* @property array SalaryCap
|
||||||
|
* @property string FoundationYear
|
||||||
* @method Player Captain()
|
* @method Player Captain()
|
||||||
* @method Player Founder()
|
* @method Player Founder()
|
||||||
* @method Player HasOneRelationship()
|
* @method Player HasOneRelationship()
|
||||||
@ -27,7 +29,7 @@ class Team extends DataObject implements TestOnly
|
|||||||
|
|
||||||
private static $db = array(
|
private static $db = array(
|
||||||
'Title' => 'Varchar',
|
'Title' => 'Varchar',
|
||||||
'DatabaseField' => 'HTMLVarchar'
|
'DatabaseField' => 'HTMLVarchar',
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $has_one = array(
|
private static $has_one = array(
|
||||||
|
Loading…
Reference in New Issue
Block a user