Skip to content

Commit

Permalink
moved escaping to connection, better escapes for many types
Browse files Browse the repository at this point in the history
  • Loading branch information
tpetry committed Apr 4, 2023
1 parent 335cd96 commit b3c68e7
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 26 deletions.
73 changes: 73 additions & 0 deletions src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1622,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)
{
if ($binary) {
return $this->escapeBinary($value);
} else if (is_numeric($value)) {
return (string) $value;
} else if (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
// 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.');
}
}
42 changes: 16 additions & 26 deletions src/Illuminate/Database/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,32 +203,6 @@ public function quoteString($value)
return "'$value'";
}

/**
* Escapes a value to use it for safe SQL embedding.
*
* @param string|float|int $value
* @return string
*/
public function escape($value)
{
if (null === $this->connection) {
throw new RuntimeException('The grammar has no connection to escape any value.');
}

// 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 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', (string) $value)) {
throw new RuntimeException('The value contains an invalid UTF-8 byte sequence.');
}

return $this->connection->getReadPdo()->quote($value);
}

/**
* Determine if the given value is a raw expression.
*
Expand Down Expand Up @@ -300,4 +274,20 @@ public function setConnection($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.');
}

return $this->connection->escape($value, $binary);
}
}
12 changes: 12 additions & 0 deletions src/Illuminate/Database/MySqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,16 @@ 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}'";
}
}
24 changes: 24 additions & 0 deletions src/Illuminate/Database/PostgresConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,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";
}

/**
* Escapes a bool value for safe SQL embedding.
*
* @param bool $value
* @return string
*/
protected function escapeBool($value)
{
return $value ? 'true' : 'false';
}
}
13 changes: 13 additions & 0 deletions src/Illuminate/Database/SQLiteConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,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}'";
}
}
13 changes: 13 additions & 0 deletions src/Illuminate/Database/SqlServerConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,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));
}

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");
}
}
54 changes: 54 additions & 0 deletions tests/Integration/Database/Postgres/EscapeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Illuminate\Tests\Integration\Database\Postgres;

use RuntimeException;

/**
* @requires extension pdo_pgsql
* @requires OS Linux|Darwin
*/
class EscapeTest extends PostgresTestCase
{
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('true', $this->app['db']->escape(true));
$this->assertSame('false', $this->app['db']->escape(false));
}

public function testEscapeBinary()
{
$this->assertSame("'\\xdead00beef'::bytea", $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");
}
}
50 changes: 50 additions & 0 deletions tests/Integration/Database/SqlServer/EscapeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Illuminate\Tests\Integration\Database\SqlServer;

use RuntimeException;

class EscapeTest extends SqlServerTestCase
{
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 testEscapeBinary()
{
$this->assertSame('0xdead00beef', $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");
}
}

0 comments on commit b3c68e7

Please sign in to comment.