● Экранирование спецсимволов.
● Шаблоны запросов и placeholders.
3.1 Экранирование спецсимволов
Прежде чем передавать значения переменных формы в SQL-запросы, необходимо специальным образом экранировать в них некоторые символы (в частности, апостроф), например, поставить перед ними обратный слэш. Для вставки предназначена функция:
mysql_escape_string()
string mysql_escape_string(string $str)
Функция похожа на другую функцию addslashes(), однако она добавляет слэши перед более полным набором специальных символов. Практика показывает, что для текстовых данных можно применять и функцию addslashes() вместо mysql_escape_string(). Во многих скриптах так и делается.
По стандарту MySQL экранированию подвергаются символы, которые в РНР записываются так: "\х00", "\n", "\г", "\", ""', "" и "\х1А".
В это число входит символ с нулевым ASCII-кодом, а поэтому mysql_escape_string() допустимо применять не только для текстовых, но также и для бинарных данных. Можно, например, считать в переменную GIF-изображение (функция file_get_contents ()), а затем вставить его в базу данных, предварительно проэкранировав все спецсимволы. При извлечении картинка окажется в том же виде, в котором она была изначально.
Экранирование символов это лишь способ записи корректных SQL-выражений, не более того. С данными ничего не происходит, и они хранятся в базе без дополнительных слэшей — так, как выглядели изначально, еще до экранирования.
С использованием mysql_escape_string()код предыдущего запроса выглядит так:
mysql_query(
"DELETE FROM table WHERE name='".mysql_escape_string($name)."'" );
Это длинно, неуклюже и некрасиво.
3.2 Шаблоны запросов и placeholders
Рассмотрим другое решение.
Вместо явного экранирования и вставки переменных в запрос на их место помещают специальные маркеры (placeholders, "хранители места"), обычно выглядящие как ?.
Те же значения, которые будут подставлены вместо них, передаются отдельно, дополнительными параметрами.
С использованием гипотетической функции mysql_qwo, код которой будет представлен ниже, предыдущий запрос может быть переписан так:
mysql_qw ('DELETE FROM table WHERE name=?', $name);
Запрос стал короче и лучше защищен: теперь мы уже при написании кода не сможем случайно пропустить вызов функции mysql_escape_string() и, таким образом, попасться на уловку хакера. Все преобразования происходят автоматически, внутри функции.
В листинге lib_mysql_qw.php содержится простейшая реализация функции mysql_qw() (qw — от англ. query wrapper, "обертка для запроса").
Имеется также библиотека lib/Placeholder.php, обеспечивающая значительно более мощную поддержку языка placeholders: http://dklab.ru/chicken/30.html.
В большинстве ситуаций возможностей, предоставляемых функцией mysql_qw (), оказывается достаточно.
Листинг lib_mysql_qw.php
<?php ## Простейшая функция для работы с placeholders.
// result-set, mysql_qw ($connection_id, $query, $argl, $arg2 ...).
// - или -
// result-set mysql_qw($query, $argl, $arg2, ...)
// Функция выполняет запрос к MySQL через соединение, заданное как
// $connection_id (если не указано, то через последнее открытое).
// Параметр $query может содержать подстановочные знаки ?,
// вместо которых будут подставлены соответствующие значения
// аргументов $arg1, $arg2 и т. д. (по порядку), экранированные и
// заключенные в апострофы.
function mysql_qw()
{
// Получаем все аргументы функции.
$args = func_get_args();
// Если первый параметр имеет тип "ресурс", то это ID-соединения.
$соnn = null;
if (is_resource($args[0])) $conn = array_shift($args);
// Формируем запрос по шаблону.
$query = call_user_func_array("mysql_make_qw", $args);
// Вызываем SQL-функцию.
return $conn!==null ? mysql_query($query, $conn): mysql_query($query);
}
// string mysql_make_qw($query, $argl, $arg2,...)
// Данная функция формирует SQL-запрос по шаблону $query,
// содержащему placeholders.
function mysql_make_qw()
{
$args = func_get_args();
// Получаем в $tmp1 ССЫЛКУ на шаблон запроса.
$tmp1 =& $args[0];
$tmp1 - str_replace("%", "%%", $tmp1);
$tmp1 = str_replace("?", "%s", $tmp1);
// После этого $args[0] также окажется измененным.
// Теперь экранируем все аргументы, кроме первого.
foreach ($args as $i=>$v)
{
if (!$i) continue; // это шаблон
if (is_int($v)) continue; // целые числа не нужно экранировать
$args[$i] = "'".mysql_escape_string($v)."'";
}
//На всякий случай заполняем 20 последних аргументов недопустимыми
// значениями, чтобы в случае, если число "?" превышает количество
// параметров, выдавалась ошибка SQL-запроса (поможет при отладке).
for ($i=$c=count($args)-1; $i<$c+20; $i++)
$args[$i+1] = "UNKNOWN_PLACEHOLDER_$i";
// Формируем SQL-запрос.
return call_user_func_array("sprintf", $args);
}
?>
Если убрать поясняющие записи, то размер файла lib_mysql_qw.php уменьшится почти в три раза:
<?php ## Простейшая функция для работы с placeholders.
function mysql_qw()
{
$args = func_get_args();
$соnn = null;
if (is_resource($args[0])) $conn = array_shift($args);
$query = call_user_func_array("mysql_make_qw", $args);
return $conn!==null ? mysql_query($query, $conn): mysql_query($query);
}
function mysql_make_qw()
{
$args = func_get_args();
$tmp1 =& $args[0];
$tmp1 - str_replace("%", "%%", $tmp1);
$tmp1 = str_replace("?", "%s", $tmp1);
foreach ($args as $i=>$v)
{
if (!$i) continue;
if (is_int($v)) continue;
$args[$i] = "'".mysql_escape_string($v)."'";
}
for ($i=$c=count($args)-1; $i<$c+20; $i++)
$args[$i+1] = "UNKNOWN_PLACEHOLDER_$i";
return call_user_func_array("sprintf", $args);
}
?>
Функция sprintf() воспринимает символ % как управляющий. Чтобы отменить его специальное действие, необходимо его удвоить, что и делается в функции. Затем ? заменяется на %s, для sprintf() это означает "взять очередной строковый аргумент".
Для удобства тестирования этого кода главная функция разбита на две, выделен код замены подстановочных знаков в функцию mysql_make_qw().
В листинге test_qw.php приведен пример того, как будут выглядеть SQL-запросы после подстановки placeholders.
Листинг test_qw.php
<?php
require_once "lib_mysql_qw.php";
require_once "mysql_connect.php";
// Представим, что мы - хакеры...
$name = "' OR '1";
// Допустимый запрос.
echo mysql_make_qw('DELETE FROM people WHERE name=?', $name)."<br>";
// Недопустимый запрос.
echo mysql_make_qw('DELETE FROM people WHERE name=? OR ?', $name)."<br>";
// Вот как выглядит выполнение запроса.
mysql_qw('DELETE FROM people WHERE name=? OR ?', $name)
or die(mysql_error());
?>
В результате работы скрипта будет сгенерирована следующая страница:
DELETE FROM people WHERE name='\' OR \'1'
DELETE FROM people WHERE name=' \ ' OR \ ' 1' OR id=UNKNOWN_PLACEHOLDER_l
Unknown column 'UNKNOWN_PLACEHOLDER_1' in 'where clause1
Перед апострофами в данных появились слэши, a placeholder, которому "не хватило" аргументов функции, оказался замененным на строчку UNKNOWN_PLACEHOLDER_l.
Теперь любая попытка выполнения такого запроса заранее обречена на неудачу (о чем говорит последнее диагностическое сообщение, сгенерированное вызовом die()), что является важным подспорьем при отладке сценариев.
3.3 Пример применения СУБД MySQL
Рассмотрим некоторые приемы, которые удобно применять в сценариях, требующих обращений к базе данных на примере гостевой книги. С книгой можно проделывать следующие два действия:
● добавлять новую запись; при этом она помечается текущей датой и помещается в таблицу базы данных;
● удалять некоторую запись по ее идентификатору.
Скрипт упрощен: удалять записи позволяется любому пользователю, а не только администратору сайта. При необходимости ограничить права легко: достаточно вставить в скрипт соответствующие проверки.
Листинг guestbook.php
<? php ## Простейшая гостевая книга.
require_once "mysql_connect.php";
require_once "lib_mysql_qw.php";
// Имя таблицы.
define("TBLNAME", "guestbook");
// Создаем таблицу, если она еще не существует.
mysql_qw ('CREATE TABLE IF NOT EXISTS '.TBLNAME.' (
id INT AUTO_INCREMENT PRIMARY KEY,
stamp TIMESTAMP,
name VARCHAR(60),
text TEXT
)
')
or die(mysql_error()) ;
// Обрабатываем кнопки и действия.
if (@$_REQUEST['doAdd'])
{
// Получаем данные из формы.
$element = $_REQUEST['element'];
// Удаляем слэши в данных, которые РНР вставил в режиме
// magic_quotes_gpc (если он включен).
if (ini_get("magic_quotes_gpc"))
$element = array_map('stripslashes', $element);
// Вставляем запись.
mysql_qw(
'INSERT INTO '.TBLNAME. 'SET name=?, text"?',
$element['name'], $element['text']
)
or die(mysql_error());
// Выполняем "самопереадресацию", чтобы при нажатии кнопки
// "Обновить" в браузере сообщение не добавлялось снова и снова.
Header ("Location: {$_SERVER['SCRIPT_NAME']}?".time());
exit ();
}
// Удаление сообщения с указанным ID.
if ($delid = @$_REQUEST['delete'])
{
mysql_qw ('DELETE FROM '.TBLNAME.' WHERE id=?', $delid)
or die(mysql_error());
}
// Выбираем все записи из таблицы, начиная с самой новой.
$result = mysql_qw('
-- Функция MySQL UNIX_TIMESTAMP() конвертирует, timestamp
-- из формата MySQL в число секунд с начала эпохи Unix.
SELECT *, UNIX_TIMESTAMP(stamp) AS stamp
FROM ' . TBLNAME. '
ORDER BY stamp DESC
')
or die(mysql_error());
for ($book=array();
$row=mysql_fetch_array($result);
$book[]=$row);
?>
<! -- Далее идет шаблон книги. -->
<form action="" method="post">
<table>
<tr valign="top">
<td>Baшe имя:</td>
<td><input type ="text" name="element [name] "></td>
</tr>
<tr valign="top">
<td>Teкст сообщения:</td>
<td><textarea name="element[text]" cols="60" rows="5"></textarea></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="doAdd" value="Добавить"</td>
</table>
</form>
<hr>
<?
foreach($book as $element)
{
?>
<b>
<? =date ("d.m.Y", $element ['stamp'])?>
<? =htmlspecialchars ($element ['name'])?>
</b>
написал:
<a href="<?=$_SERVER['SCRIPT_NAME']?>?delete=<?=$element['id']
?>
">
[удалить]</a>
<br>
<blockquote>
<?=n12br(htmlspecialchars($element['text']))
?>
</blockquote>
<hr>
<?
}
?>
Этот скрипт использует удобные на практике приемы.