Skip to content

Instantly share code, notes, and snippets.

@craigfrancis
Last active May 19, 2021 15:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save craigfrancis/5b057414de4119bcc80163ac669a7360 to your computer and use it in GitHub Desktop.
Save craigfrancis/5b057414de4119bcc80163ac669a7360 to your computer and use it in GitHub Desktop.
Demo of is_literal()
<?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