is_literal()
A proposed function for PHP
Craig Francis
is_literal()
To distinguish between safe strings,
which have been defined in your PHP script.
is_literal()
To distinguish between safe strings,
which have been defined in your PHP script.
VS unsafe strings,
which have been tainted by external sources.
The Problem
The Problem
The Problem
The Problem
Version 5.4.1
April 29, 2020
The Problem
Version 5.4.2
June 10, 2020
is_literal() is not the same as Taint Checking.
The Problem
Injection problems are rarely solved by Static Analysis.
The Problem
Injection problems are rarely solved by Static Analysis.
Still do use this.
The Problem
Injection problems are rarely solved by Static Analysis.
But it's hard to follow every variable from Source to Sink.
The Problem
Injection problems are rarely solved by Static Analysis.
And don't expect every programmer to use.
The Problem
$mysqli = new mysqli('localhost', 'test', '???', 'test');

$result = $mysqli->query('SELECT * FROM user WHERE id = ' . $_GET['id']);

if ($result instanceof mysqli_result) {

print_r($result->fetch_all());

}
Static Analysis
PHPStan
$mysqli = new mysqli('localhost', 'test', '???', 'test');

$id = (string) $_GET['id'];

$result = $mysqli->query('SELECT * FROM user WHERE id = ' . $id);

if ($result instanceof mysqli_result) {

print_r($result->fetch_all());

}
Static Analysis
Avoid MixedAssignment
Psalm
$mysqli = new mysqli('localhost', 'test', '???', 'test');

$id = (string) $_GET['id'];

$result = $mysqli->query('SELECT * FROM user WHERE id = ' . $id);

if ($result instanceof mysqli_result) {

print_r($result->fetch_all());

}
Static Analysis
Psalm,

Taint Analysis
I'm not using "mysqli_query()"

https://github.com/vimeo/psalm/issues/4155
Avoid MixedAssignment
Google engineer Christoph Kern, 2015:

https://www.usenix.org/conference/usenixsecurity15/symposium-program/
presentation/kern
Other Implementations
Google engineer Christoph Kern, 2015:

"Developer education doesn't solve the problem"
Other Implementations
Google engineer Christoph Kern, 2015:

"Bugs are hard to find after the fact"
Other Implementations
Google engineer Christoph Kern, 2015:

"Bugs are hard to find after the fact"
"Manual Testing, Automated Testing, Static Analysis, Human Code Reviews;

... will find some of these bugs..." @04:02
Other Implementations
Google have a similar solution in Go:

https://github.com/google/go-safeweb/tree/master/safehttp

https://blogtitle.github.io/go-safe-html/
Other Implementations
HTML Injection / XSS
By Roberto Clapis
Google have a similar solution in Go:

https://github.com/google/go-safeweb/tree/master/safesql

A Query Builder, with an Append() method that only accepts compile-time
constant strings (literal or const).
Other Implementations
SQL Injection
JavaScript may get a similar solution:

https://github.com/tc39/proposal-array-is-template-object

"Distinguishing strings from a trusted developer,

from strings that may be attacker controlled"
Other Implementations
Examples
Examples
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = 1')

	 ->getQuery()

	 ->getResult();
Doctrine QueryBuilder
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = ?1')

	 ->setParameter(1, $_GET['type_id'])

	 ->getQuery()

	 ->getResult();
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = 1')

	 ->getQuery()

	 ->getResult();
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = ' . $_GET['type_id'])

	 ->getQuery()

	 ->getResult();
Examples
Examples
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = 1')

	 ->getQuery()

	 ->getResult();
Safe String, a Literal
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = ' . $_GET['type_id'])

	 ->getQuery()

	 ->getResult();
Unsafe StringSafe String
Examples
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = ' . $_GET['type_id'])

	 ->getQuery()

	 ->getResult();
Examples
'WHERE u.type_id = u.type_id'
$sql = 'SELECT u FROM User u WHERE u.type_id = ' . $_GET['type_id'];

$query = $entityManager->createQuery($sql);
Doctrine - CreateQuery
Examples
$users = UserQuery::create()->where('type_id = ' . $_GET['type_id'])->find();
Propel ORM - Where
Examples
$result = R::find('user', 'type_id = ' . $_GET['type_id']);
RedBeanPHP - Find
Examples
$template = $twig->createTemplate('<p>Hello {{ name }}</p>');

echo $template->render(['name' => $_GET['name']]);
Examples Safe String, a Literal
$template = $twig->createTemplate('<p>Hello ' . $_GET['name'] . '</p>');

echo $template->render();
Examples Unsafe String
Examples
$output = shell_exec('ls /');
Safe String, a Literal
Examples
Examples
$output = shell_exec('ls ' . $_GET['path']);
Unsafe String
Taint Checking
https://pecl.php.net/package/taint
Taint Checking
$prefix = 'Hi ';

$welcome_html = $prefix . 'Name';
Taint Checking
$prefix = 'Hi ';

$welcome_html = $prefix . 'Name';
Untainted
Taint Checking
$prefix = 'Hi ';

$welcome_html = $prefix . 'Name';
Untainted
Untainted
Untainted
Taint Checking
$prefix = 'Hi ';

$welcome_html = $prefix . 'Name';
Untainted, Safe
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Tainted
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Untainted
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Untainted
Tainted
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . $name;
Tainted, not safe
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . htmlentities($name);
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . htmlentities($name);
Un-Taint
Taint Checking
$name = $_GET['name'];

$prefix = 'Hi ';

$welcome_html = $prefix . htmlentities($name);
Untainted, should be safe.
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
Tainted
Tainted
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
Tainted Tainted
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
Un-Taint Un-Taint
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
Untainted, surely this is safe?
Taint Checking
$url = $_GET['url'];

$name = $_GET['name'];

$link_html = '<a href="' . htmlentities($url) . '">' . htmlentities($name) . '</a>';
'javascript:alert("hi")'
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);
Tainted
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);
Untainted Un-Taint
Untainted, surely this is safe?
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);
Missing quotes
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);
"id"
Taint Checking
$sql .= 'WHERE id = ' . mysqli_real_escape_string($link, $_GET['id']);

$sql .= 'WHERE id = id';
"id"
Taint Checking
$img_html = '<img src=' . htmlentities($_GET['url']) . '>';
TaintedUntainted Untainted
Taint Checking
$img_html = '<img src=' . htmlentities($_GET['url']) . '>';
Un-Taint
Taint Checking
$img_html = '<img src=' . htmlentities($_GET['url']) . '>';
Untainted, surely this is safe?
Taint Checking
$img_html = '<img src=' . htmlentities($_GET['url']) . '>';
Missing quotes
Taint Checking
$img_html = '<img src=' . htmlentities($_GET['url']) . '>';

$img_html = '<img src=/ onerror=evil-js>';
"/ onerror=evil-js"
Taint Checking
How about character encoding issues?

The PDO Quote function clearly says it's only "theoretically safe".

https://www.php.net/pdo.quote
Summary
Do not mix safe strings (literals), with anything that may be attacker controlled.
Summary
We need a way to ensure this does not happen.
is_literal('This is a Literal');
is_literal('This is a Literal');
true
$a = 'This is a Literal';

is_literal($a);
true
$a = 'This is a Literal';

is_literal($a . 'And this');
true
$a = 'This is a Literal';

is_literal($a . $_GET['name']);
false
$a = 'This is a Literal';

is_literal($a . htmlentities($_GET['name']));
Still false
$a = 'This is a Literal';

is_literal($a . strtoupper('abc'));
Sorry, this is false - 'ABC' is no longer the string defined in the PHP script.
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = 1')

	 ->getQuery()

	 ->getResult();
Doctrine
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = 1')

	 ->getQuery()

	 ->getResult();
public function where($predicates)

{

if (!is_literal($predicates)) {

throw new Exception('Can only accept a literal');

}

...

}
Is a Safe Literal?
$users = $queryBuilder

	 ->select('u')

	 ->from('User', 'u')

	 ->where('u.type_id = ' . $_GET['type_id'])

	 ->getQuery()

	 ->getResult();
public function where($predicates)

{

if (!is_literal($predicates)) {

throw new Exception('Can only accept a literal');

}

...

}
Not a Safe Literal
$template = $twig->createTemplate('<p>Hello {{ name }}</p>');

echo $template->render(['name' => $_GET['name']]);
Twig
$template = $twig->createTemplate('<p>Hello {{ name }}</p>');

echo $template->render(['name' => $_GET['name']])

public function createTemplate(string $template, string $name = null): TemplateWrapper

{

if (!is_literal($template)) {

throw new Exception('Can only accept a literal');

}

...

}
Is a Safe Literal?
$template = $twig->createTemplate('<p>Hello ' . $_GET['name'] . '</p>');

echo $template->render()

public function createTemplate(string $template, string $name = null): TemplateWrapper

{

if (!is_literal($template)) {

throw new Exception('Can only accept a literal');

}

...

}
Not a Safe Literal
if (function_exists('is_literal') && !is_literal($a)) {

trigger_error('Can only accept a literal', E_USER_NOTICE);

}
Backwards compatibility
Notices might be safer for legacy projects.
if (!function_exists('is_literal')) {

function is_literal($variable) {

return true;

}

}
Backwards compatibility
$sql .= 'ORDER BY ' . $field_name;

What about Table and Field names?
$fields = [

'name',

'created',

'admin',

];

$field_id = array_search(($_GET['sort'] ?? 'created'), $fields);

$sql = ' ORDER BY ' . $fields[$field_id];
$sql .= 'WHERE id IN (1, 2, 3)';
What about WHERE IN?
$ids = [1, 2, 3];

$sql .= 'WHERE id IN (' . implode(', ', $ids) . ')';

From an unknown source
$ids = [1, 2, 3];

$sql .= 'WHERE id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')';
$ids = [1, 2, 3];

$sql .= 'WHERE id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')';

$sql .= 'WHERE id IN (?, ?, ?)';
https://wiki.php.net/rfc/is_literal

https://github.com/craigfrancis/php-is-literal-rfc
Thank You
Proposed PHP function: is_literal()

Proposed PHP function: is_literal()