Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Escaping functionality within the Grammar #46558

Merged
merged 8 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 77 additions & 1 deletion src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ public function useDefaultQueryGrammar()
*/
protected function getDefaultQueryGrammar()
{
return new QueryGrammar;
$grammar = new QueryGrammar();
$grammar->setConnection($this);

return $grammar;
}

/**
Expand Down Expand Up @@ -1619,4 +1622,77 @@ public static function getResolver($driver)
{
return static::$resolvers[$driver] ?? null;
}

/**
* Escapes a value for safe SQL embedding.
*
* @param string|float|int|bool $value
* @param bool $binary
* @return string
*/
public function escape($value, $binary = false)
tpetry marked this conversation as resolved.
Show resolved Hide resolved
tpetry marked this conversation as resolved.
Show resolved Hide resolved
{
if ($binary) {
return $this->escapeBinary($value);
} elseif (is_numeric($value)) {
tpetry marked this conversation as resolved.
Show resolved Hide resolved
return (string) $value;
tpetry marked this conversation as resolved.
Show resolved Hide resolved
} elseif (is_bool($value)) {
return $this->escapeBool($value);
} else {
// As many desktop tools and other programming languages still have problems with null bytes, they are
// forbidden for textual strings to align the support of the different databases: MySQL is the only database
// supporting null bytes within a SQL string in an escaped human-readable format. While PostgreSQL doesn't
// support null bytes, all the other ones use the invisible byte that would be hard to spot when viewing or
// copying SQL queries.
if (str_contains($value, "\00")) {
throw new RuntimeException('Strings with null bytes cannot be escaped. Use the binary escape option.');
}

// The documentation of PDO::quote states that it should be theoretically safe to use a quoted string within
// a SQL query. While only being "theoretically" safe this behaviour is used within the PHP MySQL driver all
// the time as no real prepared statements are used because it is emulating prepares by default. All
tpetry marked this conversation as resolved.
Show resolved Hide resolved
// remaining known SQL injections are always some strange charset conversion tricks that start by using
// invalid UTF-8 sequences. But those attacks are fixed by setting the proper connection charset which is
// done by the standard Laravel configuration. To further secure the implementation we can scrub the value
// by checking for invalid UTF-8 sequences.
if (false === preg_match('//u', $value)) {
throw new RuntimeException('Strings with invalid UTF-8 byte sequences cannot be escaped.');
}

return $this->escapeString($value);
}
}

/**
* Escapes a string value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeString($value)
{
return $this->getPdo()->quote($value);
}

/**
* Escapes a bool value for safe SQL embedding.
*
* @param bool $value
* @return string
*/
protected function escapeBool($value)
{
return $value ? '1' : '0';
}

/**
* Escapes a binary value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeBinary($value)
{
throw new RuntimeException('The database connection has no implementation to escape binary values.');
}
}
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 = null;

/**
* The grammar table prefix.
*
Expand Down Expand Up @@ -254,4 +261,33 @@ 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;
}

/**
* Escapes a value for safe SQL embedding.
*
* @param string|float|int|bool $value
* @param bool $binary
* @return string
*/
public function escape($value, $binary = false)
{
if (null === $this->connection) {
throw new RuntimeException('The grammar does not support escaping values.');
tpetry marked this conversation as resolved.
Show resolved Hide resolved
}

return $this->connection->escape($value, $binary);
}
}
23 changes: 21 additions & 2 deletions src/Illuminate/Database/MySqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public function isMaria()
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
$grammar = new QueryGrammar();
$grammar->setConnection($this);

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

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

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

/**
Expand Down Expand Up @@ -88,4 +94,17 @@ protected function getDoctrineDriver()
{
return new MySqlDriver;
}

/**
* Escapes a binary value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeBinary($value)
{
$hex = bin2hex($value);

return "x'{$hex}'";
}
}
34 changes: 32 additions & 2 deletions src/Illuminate/Database/PostgresConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ class PostgresConnection extends Connection
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
$grammar = new QueryGrammar();
$grammar->setConnection($this);

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

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

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

/**
Expand Down Expand Up @@ -77,4 +83,28 @@ protected function getDoctrineDriver()
{
return new PostgresDriver;
}

/**
* Escapes a binary value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeBinary($value)
{
$hex = bin2hex($value);

return "'\x{$hex}'::bytea";
tpetry marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Escapes a bool value for safe SQL embedding.
*
* @param bool $value
* @return string
*/
protected function escapeBool($value)
{
return $value ? 'true' : 'false';
}
}
23 changes: 21 additions & 2 deletions src/Illuminate/Database/SQLiteConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
$grammar = new QueryGrammar();
$grammar->setConnection($this);

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

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

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

/**
Expand Down Expand Up @@ -112,4 +118,17 @@ protected function getForeignKeyConstraintsConfigurationValue()
{
return $this->getConfig('foreign_key_constraints');
}

/**
* Escapes a binary value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeBinary($value)
{
$hex = bin2hex($value);

return "x'{$hex}'";
}
}
23 changes: 21 additions & 2 deletions src/Illuminate/Database/SqlServerConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ public function transaction(Closure $callback, $attempts = 1)
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
$grammar = new QueryGrammar();
$grammar->setConnection($this);

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

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

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

/**
Expand Down Expand Up @@ -120,4 +126,17 @@ protected function getDoctrineDriver()
{
return new SqlServerDriver;
}

/**
* Escapes a binary value for safe SQL embedding.
*
* @param string $value
* @return string
*/
protected function escapeBinary($value)
{
$hex = bin2hex($value);

return "0x{$hex}";
}
}
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 $value, bool $binary = false)
* @method static mixed transaction(\Closure $callback, int $attempts = 1)
* @method static void beginTransaction()
* @method static void commit()
Expand Down
54 changes: 54 additions & 0 deletions tests/Integration/Database/MySql/EscapeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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));
}
tpetry marked this conversation as resolved.
Show resolved Hide resolved

public function testEscapeBool()
{
$this->assertSame('1', $this->app['db']->escape(true));
$this->assertSame('0', $this->app['db']->escape(false));
}

public function testEscapeBinary()
{
$this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true));
}

public function testEscapeString()
{
$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");
}
}