Security context
HighGHSA-4mg9-vhxq-vm7j CWE-89Published Apr 29, 2021

SQL Server LIMIT / OFFSET SQL Injection in laravel/framework and illuminate/database

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

8.0.0 → fixed in 8.40.00 → fixed in 6.20.26

Details

### Impact Those using SQL Server with Laravel and allowing user input to be passed directly to the `limit` and `offset` functions are vulnerable to SQL injection. Other database drivers such as MySQL and Postgres are not affected by this vulnerability. ### Patches This problem has been patched on Laravel versions 6.20.26, 7.30.5, and 8.40.0. ### Workarounds You may workaround this vulnerability by ensuring that only integers are passed to the `limit` and `offset` functions, as well as the `skip` and `take` functions.

The fix

Release delta 8.0.0 → 8.40.0 (contains the fix)

· Sep 5, 2020, 06:27 AM+940210compare
src/Illuminate/Database/Eloquent/Factories/Factory.php+18 2
@@ -5,6 +5,7 @@
use Closure;
use Faker\Generator;
use Illuminate\Container\Container;
+use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@@ -182,7 +183,7 @@ public function createOne($attributes = [])
*
* @param array $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
- * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|mixed
+ * @return EloquentCollection|\Illuminate\Database\Eloquent\Model|mixed
*/
public function create($attributes = [], ?Model $parent = null)
{
@@ -205,6 +206,21 @@ public function create($attributes = [], ?Model $parent = null)
return $results;
}
+ /**
+ * Create a collection of models and persist them to the database.
+ *
+ * @param iterable $records
+ * @return \Illuminate\Database\Eloquent\Collection|mixed
+ */
+ public function createMany(iterable $records)
+ {
+ return new EloquentCollection(
+ array_map(function ($attribute) {
+ return $this->create($attribute);
+ }, $records)
+ );
+ }
+
/**
* Set the connection name on the results and store them.
*
@@ -255,7 +271,7 @@ public function makeOne($attributes = [])
*
* @param array $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
- * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|mixed
+ * @return EloquentCollection|\Illuminate\Database\Eloquent\Model|mixed
*/
public function make($attributes = [], ?Model $parent = null)
{
tests/Auth/AuthTokenGuardTest.php+11 11
@@ -27,10 +27,10 @@ public function testUserCanBeRetrievedByQueryStringVariable()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
$this->assertTrue($guard->check());
$this->assertFalse($guard->guest());
- $this->assertEquals(1, $guard->id());
+ $this->assertSame(1, $guard->id());
}
public function testTokenCanBeHashed()
@@ -45,10 +45,10 @@ public function testTokenCanBeHashed()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
$this->assertTrue($guard->check());
$this->assertFalse($guard->guest());
- $this->assertEquals(1, $guard->id());
+ $this->assertSame(1, $guard->id());
}
public function testUserCanBeRetrievedByAuthHeaders()
@@ -61,7 +61,7 @@ public function testUserCanBeRetrievedByAuthHeaders()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
}
public function testUserCanBeRetrievedByBearerToken()
@@ -74,7 +74,7 @@ public function testUserCanBeRetrievedByBearerToken()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
}
public function testValidateCanDetermineIfCredentialsAreValid()
@@ -124,7 +124,7 @@ public function testItAllowsToPassCustomRequestInSetterAndUseItForValidation()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
}
public function testUserCanBeRetrievedByBearerTokenWithCustomKey()
@@ -137,7 +137,7 @@ public function testUserCanBeRetrievedByBearerTokenWithCustomKey()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
}
public function testUserCanBeRetrievedByQueryStringVariableWithCustomKey()
@@ -152,10 +152,10 @@ public function testUserCanBeRetrievedByQueryStringVariableWithCustomKey()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
$this->assertTrue($guard->check());
$this->assertFalse($guard->guest());
- $this->assertEquals(1, $guard->id());
+ $this->assertSame(1, $guard->id());
}
public function testUserCanBeRetrievedByAuthHeadersWithCustomField()
@@ -168,7 +168,7 @@ public function testUserCanBeRetrievedByAuthHeadersWithCustomField()
$user = $guard->user();
- $this->assertEquals(1, $user->id);
+ $this->assertSame(1, $user->id);
}
public function testValidateCanDetermineIfCredentialsAreValidWithCustomKey()
(#34198)
src/Illuminate/Pagination/resources/views/tailwind.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/Database/Schema/PostgresSchemaState.php+2 2
@@ -51,7 +51,7 @@ protected function appendMigrationData(string $path)
*/
public function load($path)
{
- $process = $this->makeProcess('PGPASSWORD=$LARAVEL_LOAD_PASSWORD psql --file=$LARAVEL_LOAD_PATH --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE');
+ $process = $this->makeProcess('PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_restore --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE $LARAVEL_LOAD_PATH');
$process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [
'LARAVEL_LOAD_PATH' => $path,
@@ -65,7 +65,7 @@ public function load($path)
*/
protected function baseDumpCommand()
{
- return 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_dump --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER $LARAVEL_LOAD_DATABASE';
+ return 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_dump -Fc --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER $LARAVEL_LOAD_DATABASE';
}
/**
src/Illuminate/Database/Eloquent/Factories/Factory.php | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
tests/Database/DatabaseEloquentFactoryTest.php+21 0
@@ -125,6 +125,27 @@ public function test_make_creates_unpersisted_model_instance()
$this->assertCount(0, FactoryTestUser::all());
}
+ public function test_basic_model_attributes_can_be_created()
+ {
+ $user = FactoryTestUserFactory::new()->raw();
+ $this->assertIsArray($user);
+
+ $user = FactoryTestUserFactory::new()->raw(['name' => 'Taylor Otwell']);
+ $this->assertIsArray($user);
+ $this->assertEquals('Taylor Otwell', $user['name']);
+ }
+
+ public function test_expanded_model_attributes_can_be_created()
+ {
+ $post = FactoryTestPostFactory::new()->raw();
+ $this->assertIsArray($post);
+
+ $post = FactoryTestPostFactory::new()->raw(['title' => 'Test Title']);
+ $this->assertIsArray($post);
+ $this->assertIsInt($post['user_id']);
+ $this->assertEquals('Test Title', $post['title']);
+ }
+
public function test_after_creating_and_making_callbacks_are_called()
{
$user = FactoryTestUserFactory::new()
src/Illuminate/Database/Eloquent/Factories/Factory.php | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
src/Illuminate/Database/Schema/MySqlSchemaState.php+1 1
@@ -67,7 +67,7 @@ protected function appendMigrationData(string $path)
*/
public function load($path)
{
- $process = $this->makeProcess('mysql --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --user=$LARAVEL_LOAD_USER --password=$LARAVEL_LOAD_PASSWORD --database=$LARAVEL_LOAD_DATABASE < $LARAVEL_LOAD_PATH');
+ $process = $this->makeProcess('mysql --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"');
$process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [
'LARAVEL_LOAD_PATH' => $path,
.../Database/Eloquent/Factories/HasFactory.php | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
src/Illuminate/Database/Schema/SqliteSchemaState.php+94 0
@@ -0,0 +1,94 @@
+<?php
+
+namespace Illuminate\Database\Schema;
+
+use Exception;
+use Illuminate\Support\Str;
+use Symfony\Component\Process\Process;
+
+class SqliteSchemaState extends SchemaState
+{
+ /**
+ * Dump the database's schema into a file.
+ *
+ * @param string $path
+ *
+ * @return void
+ */
+ public function dump($path)
+ {
+ with($process = $this->makeProcess(
+ $this->baseCommand().' .schema'
+ ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [
+ //
+ ]));
+
+ $migrations = collect(preg_split("/\r\n|\n|\r/", $process->getOutput()))->filter(function ($line) {
+ return stripos($line, 'sqlite_sequence') === false &&
+ strlen($line) > 0;
+ })->all();
+
+ $this->files->put($path, implode(PHP_EOL, $migrations).PHP_EOL);
+
+ $this->appendMigrationData($path);
+ }
+
+ /**
+ * Append the migration data to the schema dump.
+ *
+ * @return void
+ */
+ protected function appendMigrationData(string $path)
+ {
+ with($process = $this->makeProcess(
+ $this->baseCommand().' ".dump \'migrations\'"'
+ ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [
+ //
+ ]));
+
+ $migrations = collect(preg_split("/\r\n|\n|\r/", $process->getOutput()))->filter(function ($line) {
+ return preg_match('/^\s*(--|INSERT\s)/iu', $line) === 1 &&
+ strlen($line) > 0;
+ })->all();
+
+ $this->files->append($path, implode(PHP_EOL, $migrations).PHP_EOL);
+ }
+
+ /**
+ * Load the given schema file into the database.
+ *
+ * @param string $path
+ *
+ * @return void
+ */
+ public function load($path)
+ {
+ $process = $this->makeProcess($this->baseCommand().' < $LARAVEL_LOAD_PATH');
+
+ $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [
+ 'LARAVEL_LOAD_PATH' => $path,
+ ]));
+ }
+
+ /**
+ * Get the base sqlite command arguments as a string.
+ *
+ * @return string
+ */
+ protected function baseCommand()
+ {
+ return 'sqlite3 $LARAVEL_LOAD_DATABASE';
+ }
+
+ /**
+ * Get the base variables for a dump / load command.
+ *
+ * @return array
+ */
+ protected function baseVariables(array $config)
+ {
+ return [
+ 'LARAVEL_LOAD_DATABASE' => $config['database'],
+ ];
+ }
+}
src/Illuminate/Database/Console/Migrations/MigrateCommand.php | 1 -
src/Illuminate/Database/SQLiteConnection.php | 1 -
src/Illuminate/Database/Schema/SqliteSchemaState.php | 4 ----
3 files changed, 6 deletions(-)
src/Illuminate/Database/Schema/MySqlSchemaState.php+2 2
@@ -17,7 +17,7 @@ class MySqlSchemaState extends SchemaState
public function dump($path)
{
$this->executeDumpProcess($this->makeProcess(
- $this->baseDumpCommand().' --routines --result-file=$LARAVEL_LOAD_PATH --no-data'
+ $this->baseDumpCommand().' --routines --result-file="${:LARAVEL_LOAD_PATH}" --no-data'
), $this->output, array_merge($this->baseVariables($this->connection->getConfig()), [
'LARAVEL_LOAD_PATH' => $path,
]));
@@ -83,7 +83,7 @@ protected function baseDumpCommand()
{
$gtidPurged = $this->connection->isMaria() ? '' : '--set-gtid-purged=OFF';
- return 'mysqldump '.$gtidPurged.' --column-statistics=0 --skip-add-drop-table --skip-add-locks --skip-comments --skip-set-charset --tz-utc --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --user=$LARAVEL_LOAD_USER --password=$LARAVEL_LOAD_PASSWORD $LARAVEL_LOAD_DATABASE';
+ return 'mysqldump '.$gtidPurged.' --column-statistics=0 --skip-add-drop-table --skip-add-locks --skip-comments --skip-set-charset --tz-utc --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" "${:LARAVEL_LOAD_DATABASE}"';
}
/**
(#34271)
src/Illuminate/Collections/Collection.php | 2 +-
tests/Support/SupportCollectionTest.php | 12 ++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
tests/Database/DatabaseEloquentFactoryTest.php+8 0
@@ -4,6 +4,7 @@
use Illuminate\Container\Container;
use Illuminate\Database\Capsule\Manager as DB;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Factories\Sequence;
@@ -95,6 +96,13 @@ public function test_basic_model_can_be_created()
$this->assertInstanceOf(Eloquent::class, $user);
$this->assertEquals('Taylor Otwell', $user->name);
+ $users = FactoryTestUserFactory::new()->createMany([
+ ['name' => 'Taylor Otwell'],
+ ['name' => 'Jeffrey Way'],
+ ]);
+ $this->assertInstanceOf(Collection::class, $users);
+ $this->assertCount(2, $users);
+
$users = FactoryTestUserFactory::times(10)->create();
$this->assertCount(10, $users);
}
src/Illuminate/Database/Eloquent/Factories/Factory.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/Database/Eloquent/Factories/Factory.php+1 1
@@ -183,7 +183,7 @@ public function createOne($attributes = [])
*
* @param array $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
- * @return EloquentCollection|\Illuminate\Database\Eloquent\Model|mixed
+ * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|mixed
*/
public function create($attributes = [], ?Model $parent = null)
{
src/Illuminate/Database/Eloquent/Factories/Factory.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/Database/Eloquent/Factories/Factory.php+1 1
@@ -271,7 +271,7 @@ public function makeOne($attributes = [])
*
* @param array $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
- * @return EloquentCollection|\Illuminate\Database\Eloquent\Model|mixed
+ * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|mixed
*/
public function make($attributes = [], ?Model $parent = null)
{
view (#34287)
src/Illuminate/Foundation/Console/OptimizeCommand.php | 1 +
1 file changed, 1 insertion(+)
src/Illuminate/Database/Eloquent/Factories/Factory.php+15 15
@@ -178,6 +178,21 @@ public function createOne($attributes = [])
return $this->count(null)->create($attributes);
}
+ /**
+ * Create a collection of models and persist them to the database.
+ *
+ * @param iterable $records
+ * @return \Illuminate\Database\Eloquent\Collection|mixed
+ */
+ public function createMany(iterable $records)
+ {
+ return new EloquentCollection(
+ array_map(function ($record) {
+ return $this->state($record)->create();
+ }, $records)
+ );
+ }
+
/**
* Create a collection of models and persist them to the database.
*
@@ -206,21 +221,6 @@ public function create($attributes = [], ?Model $parent = null)
return $results;
}
- /**
- * Create a collection of models and persist them to the database.
- *
- * @param iterable $records
- * @return \Illuminate\Database\Eloquent\Collection|mixed
- */
- public function createMany(iterable $records)
- {
- return new EloquentCollection(
- array_map(function ($record) {
- return $this->state($record)->create();
- }, $records)
- );
- }
-
/**
* Set the connection name on the results and store them.
*
AssertableJsonString (#34284)
src/Illuminate/Testing/AssertableJsonString.php | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
tests/Auth/AuthDatabaseUserProviderTest.php+2 2
@@ -28,7 +28,7 @@ public function testRetrieveByIDReturnsUserWhenUserIsFound()
$user = $provider->retrieveById(1);
$this->assertInstanceOf(GenericUser::class, $user);
- $this->assertEquals(1, $user->getAuthIdentifier());
+ $this->assertSame(1, $user->getAuthIdentifier());
$this->assertSame('Dayle', $user->name);
}
@@ -98,7 +98,7 @@ public function testRetrieveByCredentialsReturnsUserWhenUserIsFound()
$user = $provider->retrieveByCredentials(['username' => 'dayle', 'password' => 'foo', 'group' => ['one', 'two']]);
$this->assertInstanceOf(GenericUser::class, $user);
- $this->assertEquals(1, $user->getAuthIdentifier());
+ $this->assertSame(1, $user->getAuthIdentifier());
$this->assertSame('taylor', $user->name);
}
src/Illuminate/Database/Console/Factories/FactoryMakeCommand.php+10 5
@@ -66,7 +66,14 @@ protected function buildClass($name)
$model = class_basename($namespaceModel);
+ if (Str::startsWith($namespaceModel, 'App\\Models')) {
+ $namespace = Str::beforeLast('Database\\Factories\\'.Str::after($namespaceModel, 'App\\Models\\'), '\\');
+ } else {
+ $namespace = 'Database\\Factories';
+ }
+
$replace = [
+ '{{ factoryNamespace }}' => $namespace,
'NamespacedDummyModel' => $namespaceModel,
'{{ namespacedModel }}' => $namespaceModel,
'{{namespacedModel}}' => $namespaceModel,
@@ -88,13 +95,11 @@ protected function buildClass($name)
*/
protected function getPath($name)
{
- $name = str_replace(
- ['\\', '/'], '', $this->argument('name')
- );
+ $name = Str::replaceFirst('App\\', '', $name);
- $name = Str::finish($name, 'Factory');
+ $name = Str::finish($this->argument('name'), 'Factory');
- return $this->laravel->databasePath()."/factories/{$name}.php";
+ return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php';
}
/**
src/Illuminate/Database/Eloquent/Factories/Factory.php+12 13
@@ -166,6 +166,18 @@ public function configure()
return $this;
}
+ /**
+ * Get the raw attributes generated by the factory.
+ *
+ * @param array $attributes
+ * @param \Illuminate\Database\Eloquent\Model|null $parent
+ * @return array
+ */
+ public function raw($attributes = [], ?Model $parent = null)
+ {
+ return $this->state($attributes)->getExpandedAttributes($parent);
+ }
+
/**
* Create a single model and persist it to the database.
*
@@ -282,19 +294,6 @@ public function make($attributes = [], ?Model $parent = null)
return $instances;
}
- /**
- * Get the raw attributes generated from the factory.
- *
- * @param array $attributes
- * @param \Illuminate\Database\Eloquent\Model|null $parent
- * @return array
- */
- public function raw($attributes = [], ?Model $parent = null)
- {
- return $this->state($attributes)
- ->getExpandedAttributes($parent);
- }
-
/**
* Make an instance of the model with the given attributes.
*
src/Illuminate/Foundation/Console/stubs/maintenance-mode.stub | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php+14 0
@@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\DB;
use Illuminate\Testing\Constraints\CountInDatabase;
use Illuminate\Testing\Constraints\HasInDatabase;
use Illuminate\Testing\Constraints\SoftDeletedInDatabase;
@@ -118,6 +119,19 @@ protected function isSoftDeletableModel($model)
&& in_array(SoftDeletes::class, class_uses_recursive($model));
}
+ /**
+ * Cast a JSON string to a database compatible type.
+ *
+ * @param array|string $value
+ * @return \Illuminate\Database\Query\Expression
+ */
+ public function castAsJson($value)
+ {
+ $value = is_array($value) ? json_encode($value) : $value;
+
+ return DB::raw("CAST('$value' AS JSON)");
+ }
+
/**
* Get the database connection.
*
composer.json | 2 +-
src/Illuminate/Support/composer.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
More files changed — see the full commit.

References