diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index b9c8e16710be..b598c95eb506 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -257,7 +257,9 @@ public function useDefaultQueryGrammar() */ protected function getDefaultQueryGrammar() { - return new QueryGrammar; + ($grammar = new QueryGrammar)->setConnection($this); + + return $grammar; } /** @@ -1040,6 +1042,69 @@ public function raw($value) return new Expression($value); } + /** + * Escape a value for safe SQL embedding. + * + * @param string|float|int|bool|null $value + * @param bool $binary + * @return string + */ + public function escape($value, $binary = false) + { + if ($value === null) { + return 'null'; + } elseif ($binary) { + return $this->escapeBinary($value); + } elseif (is_int($value) || is_float($value)) { + return (string) $value; + } elseif (is_bool($value)) { + return $this->escapeBool($value); + } else { + if (str_contains($value, "\00")) { + throw new RuntimeException('Strings with null bytes cannot be escaped. Use the binary escape option.'); + } + + if (preg_match('//u', $value) === false) { + throw new RuntimeException('Strings with invalid UTF-8 byte sequences cannot be escaped.'); + } + + return $this->escapeString($value); + } + } + + /** + * Escape a string value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeString($value) + { + return $this->getPdo()->quote($value); + } + + /** + * Escape a boolean value for safe SQL embedding. + * + * @param bool $value + * @return string + */ + protected function escapeBool($value) + { + return $value ? '1' : '0'; + } + + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + throw new RuntimeException('The database connection does not support escaping binary values.'); + } + /** * Determine if the database connection has modified any database records. * diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index bba1e5831c6a..0d2b0eff42a7 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -10,6 +10,13 @@ abstract class Grammar { use Macroable; + /** + * The connection used for escaping values. + * + * @var \Illuminate\Database\Connection + */ + protected $connection; + /** * The grammar table prefix. * @@ -196,6 +203,22 @@ public function quoteString($value) return "'$value'"; } + /** + * Escapes a value for safe SQL embedding. + * + * @param string|float|int|bool|null $value + * @param bool $binary + * @return string + */ + public function escape($value, $binary = false) + { + if (is_null($this->connection)) { + throw new RuntimeException("The database driver's grammar implementation does not support escaping values."); + } + + return $this->connection->escape($value, $binary); + } + /** * Determine if the given value is a raw expression. * @@ -254,4 +277,17 @@ public function setTablePrefix($prefix) return $this; } + + /** + * Set the grammar's database connection. + * + * @param \Illuminate\Database\Connection $prefix + * @return $this + */ + public function setConnection($connection) + { + $this->connection = $connection; + + return $this; + } } diff --git a/src/Illuminate/Database/MySqlConnection.php b/src/Illuminate/Database/MySqlConnection.php index 54e3d473d580..2f87b16f5afe 100755 --- a/src/Illuminate/Database/MySqlConnection.php +++ b/src/Illuminate/Database/MySqlConnection.php @@ -13,6 +13,19 @@ class MySqlConnection extends Connection { + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + $hex = bin2hex($value); + + return "x'{$hex}'"; + } + /** * Determine if the connected database is a MariaDB database. * @@ -30,7 +43,9 @@ public function isMaria() */ protected function getDefaultQueryGrammar() { - return $this->withTablePrefix(new QueryGrammar); + ($grammar = new QueryGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** @@ -54,7 +69,9 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return $this->withTablePrefix(new SchemaGrammar); + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** diff --git a/src/Illuminate/Database/PostgresConnection.php b/src/Illuminate/Database/PostgresConnection.php index f750f64e6d08..a03b29e3bec2 100755 --- a/src/Illuminate/Database/PostgresConnection.php +++ b/src/Illuminate/Database/PostgresConnection.php @@ -12,6 +12,30 @@ class PostgresConnection extends Connection { + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + $hex = bin2hex($value); + + return "'\x{$hex}'::bytea"; + } + + /** + * Escape a bool value for safe SQL embedding. + * + * @param bool $value + * @return string + */ + protected function escapeBool($value) + { + return $value ? 'true' : 'false'; + } + /** * Get the default query grammar instance. * @@ -19,7 +43,9 @@ class PostgresConnection extends Connection */ protected function getDefaultQueryGrammar() { - return $this->withTablePrefix(new QueryGrammar); + ($grammar = new QueryGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** @@ -43,7 +69,9 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return $this->withTablePrefix(new SchemaGrammar); + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index 59b5edb210b2..6e9df07e97ba 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -36,6 +36,19 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf : $this->getSchemaBuilder()->disableForeignKeyConstraints(); } + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + $hex = bin2hex($value); + + return "x'{$hex}'"; + } + /** * Get the default query grammar instance. * @@ -43,7 +56,9 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf */ protected function getDefaultQueryGrammar() { - return $this->withTablePrefix(new QueryGrammar); + ($grammar = new QueryGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** @@ -67,7 +82,9 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return $this->withTablePrefix(new SchemaGrammar); + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index feb4577bc9b1..57d2b20402e0 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -54,6 +54,19 @@ public function transaction(Closure $callback, $attempts = 1) } } + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + $hex = bin2hex($value); + + return "0x{$hex}"; + } + /** * Get the default query grammar instance. * @@ -61,7 +74,9 @@ public function transaction(Closure $callback, $attempts = 1) */ protected function getDefaultQueryGrammar() { - return $this->withTablePrefix(new QueryGrammar); + ($grammar = new QueryGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** @@ -85,7 +100,9 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return $this->withTablePrefix(new SchemaGrammar); + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); } /** diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 5053897798cd..c71b31d29c68 100755 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -98,6 +98,7 @@ * @method static \Illuminate\Database\Grammar withTablePrefix(\Illuminate\Database\Grammar $grammar) * @method static void resolverFor(string $driver, \Closure $callback) * @method static mixed getResolver(string $driver) + * @method static string escape(string|float|int|bool|null $value, bool $binary = false) * @method static mixed transaction(\Closure $callback, int $attempts = 1) * @method static void beginTransaction() * @method static void commit() diff --git a/tests/Integration/Database/MySql/EscapeTest.php b/tests/Integration/Database/MySql/EscapeTest.php new file mode 100644 index 000000000000..9ad6d6e8a41f --- /dev/null +++ b/tests/Integration/Database/MySql/EscapeTest.php @@ -0,0 +1,64 @@ +assertSame('42', $this->app['db']->escape(42)); + $this->assertSame('-6', $this->app['db']->escape(-6)); + } + + public function testEscapeFloat() + { + $this->assertSame('3.14159', $this->app['db']->escape(3.14159)); + $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159)); + } + + public function testEscapeBool() + { + $this->assertSame('1', $this->app['db']->escape(true)); + $this->assertSame('0', $this->app['db']->escape(false)); + } + + public function testEscapeNull() + { + $this->assertSame('null', $this->app['db']->escape(null)); + $this->assertSame('null', $this->app['db']->escape(null, true)); + } + + public function testEscapeBinary() + { + $this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true)); + } + + public function testEscapeString() + { + $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647')); + $this->assertSame("'true'", $this->app['db']->escape('true')); + $this->assertSame("'false'", $this->app['db']->escape('false')); + $this->assertSame("'null'", $this->app['db']->escape('null')); + $this->assertSame("'Hello\'World'", $this->app['db']->escape("Hello'World")); + } + + public function testEscapeStringInvalidUtf8() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte"); + } + + public function testEscapeStringNullByte() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding a \00 byte"); + } +} diff --git a/tests/Integration/Database/Postgres/EscapeTest.php b/tests/Integration/Database/Postgres/EscapeTest.php new file mode 100644 index 000000000000..dc382b4a1143 --- /dev/null +++ b/tests/Integration/Database/Postgres/EscapeTest.php @@ -0,0 +1,64 @@ +assertSame('42', $this->app['db']->escape(42)); + $this->assertSame('-6', $this->app['db']->escape(-6)); + } + + public function testEscapeFloat() + { + $this->assertSame('3.14159', $this->app['db']->escape(3.14159)); + $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159)); + } + + public function testEscapeBool() + { + $this->assertSame('true', $this->app['db']->escape(true)); + $this->assertSame('false', $this->app['db']->escape(false)); + } + + public function testEscapeNull() + { + $this->assertSame('null', $this->app['db']->escape(null)); + $this->assertSame('null', $this->app['db']->escape(null, true)); + } + + public function testEscapeBinary() + { + $this->assertSame("'\\xdead00beef'::bytea", $this->app['db']->escape(hex2bin('dead00beef'), true)); + } + + public function testEscapeString() + { + $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647')); + $this->assertSame("'true'", $this->app['db']->escape('true')); + $this->assertSame("'false'", $this->app['db']->escape('false')); + $this->assertSame("'null'", $this->app['db']->escape('null')); + $this->assertSame("'Hello''World'", $this->app['db']->escape("Hello'World")); + } + + public function testEscapeStringInvalidUtf8() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte"); + } + + public function testEscapeStringNullByte() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding a \00 byte"); + } +} diff --git a/tests/Integration/Database/SqlServer/EscapeTest.php b/tests/Integration/Database/SqlServer/EscapeTest.php new file mode 100644 index 000000000000..77037d1cc810 --- /dev/null +++ b/tests/Integration/Database/SqlServer/EscapeTest.php @@ -0,0 +1,60 @@ +assertSame('42', $this->app['db']->escape(42)); + $this->assertSame('-6', $this->app['db']->escape(-6)); + } + + public function testEscapeFloat() + { + $this->assertSame('3.14159', $this->app['db']->escape(3.14159)); + $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159)); + } + + public function testEscapeBool() + { + $this->assertSame('1', $this->app['db']->escape(true)); + $this->assertSame('0', $this->app['db']->escape(false)); + } + + public function testEscapeNull() + { + $this->assertSame('null', $this->app['db']->escape(null)); + $this->assertSame('null', $this->app['db']->escape(null, true)); + } + + public function testEscapeBinary() + { + $this->assertSame('0xdead00beef', $this->app['db']->escape(hex2bin('dead00beef'), true)); + } + + public function testEscapeString() + { + $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647')); + $this->assertSame("'true'", $this->app['db']->escape('true')); + $this->assertSame("'false'", $this->app['db']->escape('false')); + $this->assertSame("'null'", $this->app['db']->escape('null')); + $this->assertSame("'Hello''World'", $this->app['db']->escape("Hello'World")); + } + + public function testEscapeStringInvalidUtf8() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte"); + } + + public function testEscapeStringNullByte() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding a \00 byte"); + } +} diff --git a/tests/Integration/Database/Sqlite/EscapeTest.php b/tests/Integration/Database/Sqlite/EscapeTest.php new file mode 100644 index 000000000000..f642c6fe3363 --- /dev/null +++ b/tests/Integration/Database/Sqlite/EscapeTest.php @@ -0,0 +1,76 @@ +markTestSkipped('Test requires a Sqlite connection.'); + } + + $app['config']->set('database.default', 'conn1'); + + $app['config']->set('database.connections.conn1', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + public function testEscapeInt() + { + $this->assertSame('42', $this->app['db']->escape(42)); + $this->assertSame('-6', $this->app['db']->escape(-6)); + } + + public function testEscapeFloat() + { + $this->assertSame('3.14159', $this->app['db']->escape(3.14159)); + $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159)); + } + + public function testEscapeBool() + { + $this->assertSame('1', $this->app['db']->escape(true)); + $this->assertSame('0', $this->app['db']->escape(false)); + } + + public function testEscapeNull() + { + $this->assertSame('null', $this->app['db']->escape(null)); + $this->assertSame('null', $this->app['db']->escape(null, true)); + } + + public function testEscapeBinary() + { + $this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true)); + } + + public function testEscapeString() + { + $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647')); + $this->assertSame("'true'", $this->app['db']->escape('true')); + $this->assertSame("'false'", $this->app['db']->escape('false')); + $this->assertSame("'null'", $this->app['db']->escape('null')); + $this->assertSame("'Hello''World'", $this->app['db']->escape("Hello'World")); + } + + public function testEscapeStringInvalidUtf8() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte"); + } + + public function testEscapeStringNullByte() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape("I am hiding a \00 byte"); + } +}