-
-
Save craigfrancis/5b057414de4119bcc80163ac669a7360 to your computer and use it in GitHub Desktop.
Demo of is_literal()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// https://wiki.php.net/rfc/is_literal | |
// | |
// If the RFC is accepted, you will be able to | |
// check a variable contains a programmer defined | |
// string. | |
// | |
// Useful for all programmers (especially newbies), | |
// to ensure they do not introduce vulnerabilities | |
// like SQL Injection, many cases of XSS, and | |
// Command Line Injection. | |
// | |
// How? You can simply check the SQL, HTML, CLI string | |
// only contains a value from your PHP scripts. | |
// Then rely on your database (parameterised queries) or | |
// templating engine (context aware escaping) to handle | |
// the user supplied values. | |
var_dump(is_literal('Example')); // true | |
var_dump(is_literal(sprintf('Example'))); // false, modified output from a function is not a literal. | |
echo "\n"; | |
$a = 'Hello'; | |
$b = 'World'; | |
var_dump(is_literal($a)); // true | |
var_dump(is_literal($a . $b)); // true | |
var_dump(is_literal("Hi $b")); // true | |
echo "\n"; | |
var_dump(is_literal($_GET['id'] ?? NULL)); // false | |
var_dump(is_literal('WHERE id = ' . intval($_GET['id'] ?? NULL))); // false | |
var_dump(is_literal('<input name="q" value="' . ($_GET['q'] ?? NULL) . '" />')); // false | |
var_dump(is_literal('/bin/rm -rf ' . ($_GET['path'] ?? NULL))); // false | |
var_dump(is_literal(rand(0, 10))); // false | |
var_dump(is_literal(sprintf('Example %d', true))); // false | |
echo "\n"; | |
function example($input) { | |
if (!is_literal($input)) { | |
throw new Exception('Non-literal detected!'); | |
} | |
return $input; | |
} | |
var_dump(example($a)); // Prints 'Hello' | |
var_dump(example(example($a))); // Prints 'Hello' (still the same literal) | |
try { | |
var_dump(example(strtoupper($a))); // Exception thrown, value modified. | |
} catch (exception $e) { | |
var_dump(false); | |
} | |
echo "\n"; | |
//-------------------------------------------------- | |
// How an ORM or HTML Templating system can use: | |
class db { | |
private $pdo; | |
private $protection_level = 2; // At first this should default to 1 (log warnings). | |
function __construct() { | |
$this->pdo = new PDO('mysql:dbname=...;host=...', '...', '...', [PDO::ATTR_EMULATE_PREPARES => false]); | |
} | |
function literal_check($var) { | |
if (!function_exists('is_literal') || is_literal($var)) { | |
// Not supported (PHP 8.0), or is a programmer defined string. | |
} else if ($var instanceof unsafe_value) { | |
// Not ideal, but at least you know this one is unsafe. | |
} else if ($this->protection_level === 0) { | |
// Programmer aware, and is choosing to bypass this check. | |
} else if ($this->protection_level === 1) { | |
trigger_error('Non-literal detected!', E_USER_WARNING); | |
} else { | |
throw new Exception('Non-literal detected!'); | |
} | |
} | |
function unsafe_disable_injection_protection() { | |
$this->protection_level = 0; // Not recommended, try a `new unsafe_value('XXX')` for special cases. | |
} | |
function query($sql, $parameters = [], $aliases = []) { | |
$this->literal_check($sql); | |
foreach ($aliases as $name => $value) { | |
if (!preg_match('/^[a-z0-9_]+$/', $name)) { | |
throw new Exception('Invalid alias name "' . $name . '"'); | |
} else if (!preg_match('/^[a-z0-9_]+$/', $value)) { | |
throw new Exception('Invalid alias value "' . $value . '"'); | |
} else { | |
$sql = str_replace('{' . $name . '}', '`' . $value . '`', $sql); | |
} | |
} | |
$statement = $this->pdo->prepare($sql); | |
$statement->execute($parameters); | |
return $statement->fetchAll(); | |
} | |
} | |
$db = new db(); | |
//-------------------------------------------------- | |
// While you should never need it; the example | |
// library above will also accept this value-object: | |
class unsafe_value { | |
private $value = ''; | |
function __construct($unsafe_value) { | |
$this->value = $unsafe_value; | |
} | |
function __toString() { | |
return $this->value; | |
} | |
} | |
//-------------------------------------------------- | |
// Normal use: | |
$id = sprintf('1'); // Using sprintf() so it's not marked as a literal, e.g. $_GET['id'] | |
var_dump($db->query('SELECT name FROM user WHERE id = ?', [$id])); | |
try { | |
var_dump($db->query('SELECT name FROM user WHERE id = ' . $id)); | |
} catch (exception $e) { | |
var_dump('Caught SQLi vulnerability!'); | |
} | |
// Doctrine can protect itself: | |
// | |
// https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/security.html | |
// | |
// For example: | |
// | |
// $qb->select('u') | |
// ->from('User', 'u') | |
// ->where('u.id = ' . $_GET['id']) <-- INSECURE | |
// | |
// Same with Drupal: | |
// | |
// https://www.drupal.org/node/101496 | |
echo "\n"; | |
//-------------------------------------------------- | |
// Complex example, and still doesn't use unsafe_value: | |
$parameters = []; | |
$where_sql = 'u.deleted IS NULL'; | |
$name = ($_GET['name'] ?? NULL); | |
if ($name) { | |
$where_sql .= ' AND u.name LIKE ?'; | |
$parameters[] = '%' . $name . '%'; | |
} | |
$ids = ($_GET['ids'] ?? '1,2,3'); | |
$ids = array_filter(explode(',', $ids)); | |
if (count($ids) > 0) { | |
$in_sql = '?'; | |
for ($k = count($ids); $k > 1; $k--) { | |
$in_sql .= ',?'; // Could also use literal_implode() | |
} | |
$where_sql .= ' AND u.id IN (' . $in_sql . ')'; // Your database abstraction could simplify this. | |
$parameters = array_merge($parameters, $ids); | |
} | |
$sql = ' | |
SELECT | |
u.name, | |
u.email | |
FROM | |
user AS u | |
WHERE | |
' . $where_sql; | |
$order_fields = ['name', 'email']; | |
$order_id = array_search(($_GET['sort'] ?? NULL), $order_fields); | |
$sql .= ' | |
ORDER BY ' . $order_fields[$order_id]; // Limited to known-safe fields. | |
$sql .= ' | |
LIMIT | |
?, ?'; | |
$parameters[] = 0; | |
$parameters[] = 3; | |
var_dump($sql, $parameters); | |
var_dump($db->query($sql, $parameters)); | |
echo "\n"; | |
//-------------------------------------------------- | |
// And if table/field/etc names cannot be included | |
// in your PHP script (e.g. a CMS like Drupal), it's | |
// still possible to use: | |
$parameters = []; | |
$aliases = [ | |
'with_1' => sprintf('w1'), // Using sprintf() so it's not marked as a literal. | |
'table_1' => sprintf('user'), | |
'field_1' => sprintf('email'), | |
'field_2' => sprintf('dob'), // ... All of these are user defined fields. | |
]; | |
$with_sql = '{with_1} AS (SELECT id, name, type, {field_1} as f1, deleted FROM {table_1})'; | |
$sql = " | |
WITH | |
$with_sql | |
SELECT | |
t.name, | |
t.f1 | |
FROM | |
{with_1} AS t | |
WHERE | |
t.type = ? AND | |
t.deleted IS NULL"; | |
$parameters[] = ($_GET['type'] ?? 'admin'); | |
var_dump($sql, $parameters, $aliases); | |
var_dump($db->query($sql, $parameters, $aliases)); | |
echo "\n"; | |
//-------------------------------------------------- | |
// And, with the appropriate processing (future work), | |
// how this can be used in other contexts. | |
$query = ($_GET['q'] ?? NULL); | |
function html_template() { return NULL; } | |
function run_command() { return NULL; } | |
function run_eval() { return NULL; } | |
var_dump(html_template('<input name="q" value="?" />', [ | |
$query | |
])); | |
var_dump(run_command('/my/script.sh ?', [ | |
$query, | |
])); | |
var_dump(run_eval('echo ?;', [ | |
$query, | |
])); | |
// $additional_params in mail() | |
// $expression in $xpath->query() | |
// $pattern in preg_match() | |
// etc... | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment