Skip to content

Commit

Permalink
[10.x] Escaping functionality within the Grammar (#46558)
Browse files Browse the repository at this point in the history
* grammars can escape values for safe embedding in sql queries

* moved escaping to connection, better escapes for many types

* styleci

* styleci

* type and doc improvements

* styleci

* null doctype missing

* formatting

---------

Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
tpetry and taylorotwell committed May 24, 2023
1 parent 504f255 commit e953137
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 9 deletions.
67 changes: 66 additions & 1 deletion src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ public function useDefaultQueryGrammar()
*/
protected function getDefaultQueryGrammar()
{
return new QueryGrammar;
($grammar = new QueryGrammar)->setConnection($this);

return $grammar;
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
36 changes: 36 additions & 0 deletions src/Illuminate/Database/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
}
21 changes: 19 additions & 2 deletions src/Illuminate/Database/MySqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -30,7 +43,9 @@ public function isMaria()
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
($grammar = new QueryGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand All @@ -54,7 +69,9 @@ public function getSchemaBuilder()
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
($grammar = new SchemaGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand Down
32 changes: 30 additions & 2 deletions src/Illuminate/Database/PostgresConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,40 @@

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.
*
* @return \Illuminate\Database\Query\Grammars\PostgresGrammar
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
($grammar = new QueryGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand All @@ -43,7 +69,9 @@ public function getSchemaBuilder()
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
($grammar = new SchemaGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand Down
21 changes: 19 additions & 2 deletions src/Illuminate/Database/SQLiteConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,29 @@ 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.
*
* @return \Illuminate\Database\Query\Grammars\SQLiteGrammar
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
($grammar = new QueryGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand All @@ -67,7 +82,9 @@ public function getSchemaBuilder()
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
($grammar = new SchemaGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand Down
21 changes: 19 additions & 2 deletions src/Illuminate/Database/SqlServerConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,29 @@ 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.
*
* @return \Illuminate\Database\Query\Grammars\SqlServerGrammar
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
($grammar = new QueryGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand All @@ -85,7 +100,9 @@ public function getSchemaBuilder()
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
($grammar = new SchemaGrammar)->setConnection($this);

return $this->withTablePrefix($grammar);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/DB.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
64 changes: 64 additions & 0 deletions tests/Integration/Database/MySql/EscapeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Illuminate\Tests\Integration\Database\MySql;

use RuntimeException;

/**
* @requires extension pdo_mysql
* @requires OS Linux|Darwin
*/
class EscapeTest extends MySqlTestCase
{
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");
}
}

0 comments on commit e953137

Please sign in to comment.