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
111 changes: 90 additions & 21 deletions system/BaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ abstract class BaseModel
*/
protected $protectFields = true;

/**
* Whether Model should throw instead of silently discarding
* fields that are not in $allowedFields.
*/
protected bool $throwOnDisallowedFields = false;

/**
* An array of field names that are allowed
* to be set by the user in inserts/updates.
Expand Down Expand Up @@ -990,32 +996,31 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
$cleanValidationRules = $this->cleanValidationRules;
$this->cleanValidationRules = false;

if (is_array($set)) {
foreach ($set as &$row) {
$row = $this->transformDataToArray($row, 'insert');

// Validate every row.
if (! $this->skipValidation && ! $this->validate($row)) {
// Restore $cleanValidationRules
$this->cleanValidationRules = $cleanValidationRules;
try {
if (is_array($set)) {
foreach ($set as &$row) {
$row = $this->transformDataToArray($row, 'insert');

return false;
}
// Validate every row.
if (! $this->skipValidation && ! $this->validate($row)) {
return false;
}

// Must be called first so we don't
// strip out created_at values.
$row = $this->doProtectFieldsForInsert($row);
// Must be called first so we don't
// strip out created_at values.
$row = $this->doProtectFieldsForInsert($row);

// Set created_at and updated_at with same time
$date = $this->setDate();
$row = $this->setCreatedField($row, $date);
$row = $this->setUpdatedField($row, $date);
// Set created_at and updated_at with same time
$date = $this->setDate();
$row = $this->setCreatedField($row, $date);
$row = $this->setUpdatedField($row, $date);
}
}
} finally {
// Restore $cleanValidationRules
$this->cleanValidationRules = $cleanValidationRules;
}

// Restore $cleanValidationRules
$this->cleanValidationRules = $cleanValidationRules;

$eventData = ['data' => $set];

if ($this->tempAllowCallbacks) {
Expand Down Expand Up @@ -1067,7 +1072,7 @@ public function update($id = null, $row = null): bool

// Must be called first, so we don't
// strip out updated_at values.
$row = $this->doProtectFields($row);
$row = $this->doProtectFieldsForUpdate($row);

// doProtectFields() can further remove elements from
// $row, so we need to check for empty dataset again
Expand Down Expand Up @@ -1157,6 +1162,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc

// Must be called first so we don't
// strip out updated_at values.
$this->ensureNoDisallowedFields($row, $index === null ? [] : [$index]);
$row = $this->doProtectFields($row);

// Restore updateIndex value in case it was wiped out
Expand Down Expand Up @@ -1382,6 +1388,19 @@ public function protect(bool $protect = true)
return $this;
}

/**
* Sets whether or not disallowed fields should throw an exception
* instead of being discarded.
*
* @return $this
*/
public function throwOnDisallowedFields(bool $throw = true)
{
$this->throwOnDisallowedFields = $throw;

return $this;
}

/**
* Ensures that only the fields that are allowed to be updated are
* in the data array.
Expand Down Expand Up @@ -1414,6 +1433,35 @@ protected function doProtectFields(array $row): array
return $row;
}

/**
* Throws when configured to detect fields that would be discarded.
*
* @param row_array $row
* @param list<string> $ignoredFields
*
* @throws DataException
*/
protected function ensureNoDisallowedFields(array $row, array $ignoredFields = []): void
{
if (! $this->throwOnDisallowedFields || ! $this->protectFields || $this->allowedFields === []) {
return;
}

$disallowedFields = [];

foreach (array_keys($row) as $key) {
if (in_array($key, $this->allowedFields, true) || in_array($key, $ignoredFields, true)) {
continue;
}

$disallowedFields[] = $key;
}

if ($disallowedFields !== []) {
throw DataException::forDisallowedFields(static::class, $disallowedFields);
}
}

/**
* Ensures that only the fields that are allowed to be inserted are in
* the data array.
Expand All @@ -1429,6 +1477,27 @@ protected function doProtectFields(array $row): array
*/
protected function doProtectFieldsForInsert(array $row): array
{
$this->ensureNoDisallowedFields($row);

return $this->doProtectFields($row);
}

/**
* Ensures that only the fields that are allowed to be updated are in
* the data array.
*
* @used-by update() to protect against mass assignment vulnerabilities.
*
* @param row_array $row
*
* @return row_array
*
* @throws DataException
*/
protected function doProtectFieldsForUpdate(array $row): array
{
$this->ensureNoDisallowedFields($row);

return $this->doProtectFields($row);
}

Expand Down
1 change: 1 addition & 0 deletions system/Commands/Generators/Views/model.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class {class} extends Model
protected $protectFields = true;
protected $allowedFields = [];

protected bool $throwOnDisallowedFields = false;
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;

Expand Down
10 changes: 10 additions & 0 deletions system/Database/Exceptions/DataException.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public static function forInvalidAllowedFields(string $model)
return new static(lang('Database.invalidAllowedFields', [$model]));
}

/**
* @param list<string> $fields
*
* @return DataException
*/
public static function forDisallowedFields(string $model, array $fields)
{
return new static(lang('Database.disallowedFields', [$model, implode(', ', $fields)]));
}

/**
* @return DataException
*/
Expand Down
1 change: 1 addition & 0 deletions system/Language/en/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'invalidEvent' => '"{0}" is not a valid Model Event callback.',
'invalidArgument' => 'You must provide a valid "{0}".',
'invalidAllowedFields' => 'Allowed fields must be specified for model: "{0}"',
'disallowedFields' => 'Fields are not allowed for model "{0}": {1}',
'emptyDataset' => 'There is no data to {0}.',
'emptyPrimaryKey' => 'There is no primary key defined when trying to make {0}.',
'failGetFieldData' => 'Failed to get field data from database.',
Expand Down
9 changes: 9 additions & 0 deletions system/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,8 @@ protected function doProtectFieldsForInsert(array $row): array
throw DataException::forInvalidAllowedFields(static::class);
}

$this->ensureNoDisallowedFields($row, $this->useAutoIncrement === false ? [$this->primaryKey] : []);

foreach (array_keys($row) as $key) {
// Do not remove the non-auto-incrementing primary key data.
if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
Expand All @@ -781,6 +783,13 @@ protected function doProtectFieldsForInsert(array $row): array
return $row;
}

protected function doProtectFieldsForUpdate(array $row): array
{
$this->ensureNoDisallowedFields($row, [$this->primaryKey]);

return $this->doProtectFields($row);
}

/**
* Finds the first row matching attributes or inserts a new row.
*
Expand Down
Loading
Loading