Подтверждение действия

24 октября 2008

Некоторые действия (например, удаление) желательно выполнять с подтверждением. Подтверждение можно сделать двумя способами: либо две различных процедуры, первая из которых выводит форму запроса подтверждения, а вторая выполняет действия, либо одна процедура, которая проверяет, есть ли в параметрах некоторый флаг, и если его нет, выводит форму запроса подтверждения, в которой все полученные поля запроса от предыдущего вызова помещаются в скрытые поля.

Второй способ предпочтительнее по двум причинам: а) можно написать универсальную процедуру генерации скрытых полей, б) можно добавить проверку на JavaScript, которая будет запрашивать подтверждение на стороне клиента.

Пример: ссылка для удаления сообщения: <a href=”index.php?a=delete&post=1″ >Удалить</a>. Добавляем в эту ссылку событие onClick=”if (confirm(’Вы действительно хотите это удалить?’)) { this.href+=’&confirm=1′; return true; } else return false;”.

Далее  в процедуре удаления выполняем проверку Далее »

Статья о PHP-безопасности

13 октября 2008

Весьма полезная статья про типовые уязвимости PHP-скриптов: http://vingrad.ru/blogs/dominus/rokovyie-oshibki-php/ и вторая часть http://vingrad.ru/blogs/dominus/rokovyie-oshibki-php-v2/

Оптимизация MySQL-запросов

12 октября 2008

Несколько советов по оптимизации MySQL-запросов.

  1. Для изучения того, как выполняется запрос, можно использовать ключевое слово EXPLAIN (добавляется перед текстом запроса). EXPLAIN показывает, в каком порядке таблицы связываются, какие ключи при этом используются и сколько строк выбирается из каждой таблицы.
  2. Если количество строк, которое надо выбрать, заранее известно точно (например, нужна всего одна строка), в конец запроса имеет смысл добавить LIMIT 1. В этом случае при связывании каждой таблицы будет искаться только первая запись, соответствующая критерию. (Исключением являются случаи, когда все таблицы связываются по первичному/уникальному ключу, в этом случае LIMIT 1 не даст каких-либо выгод.)
  3. Если таблица содержит большие текстовые или бинарные поля, а при выводе требуется ее сортировка по какому-то другому полю (например, дате или номеру) или выборка по сложному критерию, не затрагивающему эти текстовые поля, то имеет смысл разбить ее на две таблицы, в одной из которых будет поле для сортировки, а в другой — текстовое. В этом случае сортировка/выборка значительно ускорится.
  4. При использовании LEFT JOIN таблицы связываются в том порядке, в котором они перечислены. Этим следует пользоваться для того, чтобы вынести в конец запроса “тяжелые” таблицы, которые присоединяются для получения информации и не участвуют в выборке данных по сложным критериям (т.е. привязываются к предыдущим по первичному ключу). Примером такой ситуации является таблица с текстыми/бинарными данными из п.3. В начало запроса следует выносить те таблицы, выборка по которомы значительно сокращает количество строк данных, выбираемых в следующих таблицах (это можно узнать с помощью EXPLAIN).
  5. Для выборки максимума можно воспользоваться сортировкой по искомому столбцу и LIMIT 1.
  6. При создании индексов нужно не забывать, что если для первого столбца индекса совпадает более 30% значений, то этот индекс не используется при выборке. Поэтому в ситуациях, когда таблица индексируется по двум столбцам, в одном из которых значение меняется редко, этот столбец должен обязательно идти последним. Пример: один столбец col1 — двоичный признак, в который пишутся значения 0 и 1, причем записей с 1 значительно больше, чем с 0, а второй col2 — дата, и выборка производится всех сообщений с признаком 1 в определенном диапазоне дат. Если индекс будет определен как (col1,col2), он будет проигнорирован, так как записей с 1 более 30%, и для выборки по дате придется просматривать всю таблицу. Если же определить индекс как (col2,col1), то он сработает нормально.
  7. В некоторых ситуациях, когда требуется сделать выборку по сложному условию из нескольких таблиц и часть этого условия не совпадает ни с одним индексом, может оказаться выгоднее вынести эту часть условия в HAVING, а связывание таблиц делать исключительно по индексам. Но это утверждение верно только в случаях, когда количество записей с дополнительным условием не сильно отличается от количества записей, извлеченных только по ключам.
  8. В некоторых слуачаях вместо операции UPDATE оказыается более целесообразным делать DELETE/INSERT и периодически выполнять оптимизацию по cron, чтобы уменьшить время блокировок.

Уязвимость через разрешенные теги или Зачем различать GET и POST на стороне сервера

10 октября 2008

На сайтах, где пользователям разрешается отправлять свои сообщения/комментарии с использованием некоторого ограниченного количества HTML-тегов (в частности, <img src=”"> или <a href=”"> или их BBCode-аналоги ), возможна следующая уязвимость: злоумышленник вставляет в страницу ссылку, выполняющую какое-то нежелательное действие штатными средствами скрипта (например, удаление комментария предыдущего пользователя). Далее, если на эту страницу заходит модератор или администратор, его броузер заходит по этой ссылке, пытаясь скачать вставленную картинку, а вместо этого выполняет удаление сообщения, причем даже не заметив этого факта.

Бороться с подобной уязвимостью можно двумя способами: либо при выполнении опасных действий модераторов/администраторов проверять, что запрос на выполнение действия отправлен методом POST (который существенно сложнее сфальсифицировать без применения соц. инженерии), либо генерировать при входе администратора/модератора случайный ключ, который запоминать в сессии или cookie и приписывать в качестве параметра к запросу на выполнение опасного действия.

Второй способ является более надежным и универсальным и избавляет от ограничений, связанных с POST-запросом.

Пример такой ситуации. Пусть index.php?action=delete&post=111 — ссылка для удаления комментария. Если проверок на POST-метод или ключ не выполняется, то злоумылшенник может отправить сообщение с картинкой <img src=”index.php?action=delete&post=111″>, и если в тему зайдет модератор, сообщение 111 будет удалено. При добавлении проверки по ключу ссылка для модератора принимает вид index.php?action=delete&post=111&key=случайный_ключ, при этом ключ хранится в сессии модератора и никому, кроме него неизвестен. При попытке выполнить удаление без ключа (или с неправильным ключом) действие просто не будет выполнено, в результате чего атака злоумышленника провалится.

Циклическая прокрутка картинок

10 сентября 2008

Если требуется организовать циклическую прокрутку множества картинок по сайту, то можно поступить следующим образом. Все картинки располагаются в одном div без разделителей. Затем с помощью JavaScript у самой левой (или верхней, если прокрутка вертикальная) картинки задаем постепенно увеличивающийся до ширины всей картинки отрицательный margin-left (т.е. он изменяется от 0 до -ширина_картинки), затем удаляем первую картинку из div, обнуляем ей margin и снова добавляем обратно в div, но уже как последний элемент.

Такая задача очень просто реализуется с помощью jQuery: Далее »

Простая защита от DoS-атак с помощью MySQL

8 сентября 2008

Организовать простую защиту от DoS-атак для сайтов, использующих PHP + MySQL, можно следующим образом. В MySQL создается таблица (будем называть ее ip_check) с полями lasttime, count, ip (все поля — типа INTEGER, причем ip — первичный ключ).

Сразу после подключения к базе данных выполняется проверка, есть ли для данного IP-адреса запись в таблице. Если записи нет, она создается, при этом в count пишется 1, в lasttime — текущее время. Если запись уже есть, то проверяется, когда она была сделана (поле lasttime), и если прошло менее определенного количества секунд, то поле count увеличивается на 1, в противном случае приравнивается единице, и производится обновление записи в таблице (записывается новый count и lasttime). Далее происходит проверка величины count, и если она превысила некоторое пороговое значение, выдается статус 500 (или 403 или 503), сообщение об ошибке, и выполнение скрипта завершается.

Таким образом, для выполнения проверки требуется всего одной таблица с предельно простой структурой и 2 запроса SQL. Также можно добавить еще одно поле status, в которое ставить 1, если IP-адрес забанен навсегда по каким-то причинам, и совместить проверку на DoS-атаки с проверкой на забаненные IP-адреса без добавления дополнительных запросов. При необходимости можно еще одним запросом добавить учет суммарной нагрузки за все время или нагрузки по подсетям.

Кроме того, перед завершением скрипта можно добавить проверку, сколько времени заняло его выполнение, и если оно превышает какую-то пороговую величину (например, 10 секунд), начислять этому IP-адресу “штрафные очки” (т.е. дополнительно увеличивать поле count).

В виде кода это можно представить следующим образом:

// $link -- соединение с БД, $ip_text -- IP-адрес
define('ALERT_TIME',300);
define('ALERT_COUNT',150);  

function check_dos($ip_text, $link)  {
  $ip=ip2long($ip_text);
  $res=mysql_query($link,'SELECT lasttime, count, status FROM ip_check WHERE ip="'.$ip.'"');
  if (mysql_num_rows($res)==0) { // нет такого IP
  $count=1;
  mysql_query($link,'INSERT INTO ip_check (lasttime,count,ip,status) VALUES ('.time().',1,"'.$ip.'",0)');
  }
else {
  $data=mysql_fetch_row($res);
  $status=$data[2];
  if ($status!=2 && ($data[0]<time()-ALERT_TIME || $data[1]<ALERT_COUNT)))   $count=1;
  elseif ($status!=2) { $count=$data[1]+1; if ($count>=ALERT_COUNT) $status=1; }
  if ($status!=2) {
    mysql_query($link,'UPDATE ip_check SET count='.$count,', status='.$status.', lasttime='.time().' WHERE ip="'.$ip.'"';
  }
  if ($status) {
    header($_SERVER['HTTP_PROTOCOL'].' 503 Temporary Unavailable');
    trigger_error('Превышено число допустимых запросов!',E_USER_ERROR);
  }
}

Кроме того, периодически следует очищать базу от старых записей (например, по cron). Кроме того, если ожидается большая интенсивность атак, возможно, имеет смысл заменить UPDATE-запрос на последовательность DELETE/INSERT, но в этом случае необходимо будет регулярно проводить оптимизацию таблицы.

position:fixed в MSIE

7 сентября 2008

В MSIE 6, как известно, свойство position: fixed не работает. Решается эта проблема с помощью expression:

#intb #leftmenu { position: absolute; top: expression( eval(document.documentElement.scrollTop+смещение) + "px");}

(Важно отметить, что рекомендация eval(document.body.scrollTop), которая дается в некоторых источниках, не работает в IE6 в режиме соответствия стандартам.) Чтобы блок при прокрутке не дергался, можно задать фиксированный фон для тега body. Помещаем это в условные комментарии и получаем следующее (при смещении 200px):

<style type="text/css"><!--
#intb #leftmenu { position: fixed; left: 0; top: 200px }
-->
</style>

<!--[if lt ie 7.0]>
<style type="text/css">
body { background: url('spacer.gif') no-repeat;  background-attachment: fixed; }
#intb #leftmenu { position: absolute; top: expression( eval(document.documentElement.scrollTop+200) + "px"); }
</style>
<![endif]-->

Основные идеи были заимствованы отсюда: http://www.artlebedev.ru/tools/technogrette/html/fixed_in_msie/

Информация по jQuery

5 сентября 2008

http://www.linkexchanger.su/ — отличный блог с русскоязычной информацией по использованию jQuery.

Закругленные уголки при неоднородном фоне

13 августа 2008

Об скругленных уголках на сайтах написано достаточно много. Но почти все примеры предназначены для ситуаций, когда либо фон блока, либо область, поверх которой расположен блок, являются однородными. Но бывают ситуации, когда фон блока является градиентным, а область — неоднородной (например, содержит какую-нибудь сетку).

В этом случае внешняя часть уголка делается прозрачной, внутренняя — закрашивается соответствующим градиентом, также делается полоска шириной в 1 пиксель для части блока, которая оказывается между уголками. Все это вставляется с помощью следующей CSS-конструкции:

<div id="block">
<div id="topline"><div><div></div></div></div>
Здесь идет контент блока<
div id="bottomline"><div><div></div></div></div>
</div>

Слои с помощью CSS описываются следующим образом:

#topline, #bottomline { font-size:0 }
#topline div, #bottomline div { margin: 0 0 0 19px }
#topline div div, #bottomline div div { margin: 0 19px 0 0; height: 20px } 

#topline { background-image: url('upload/lockey/corn_tl.gif'); background-repeat: no-repeat }
#topline div { background-image: url('upload/lockey/corn_tr.gif'); background-position: right; background-repeat: no-repeat; }
#topline div div { width: 732px; background: url('upload/lockey/topline.gif') #FFF; background-repeat: repeat-x } 

#bottomline { background-image: url('upload/lockey/corn_bl.gif'); background-repeat: no-repeat }
#bottomline div { background: url('upload/lockey/corn_br.gif'); background-position: right; background-repeat: no-repeat }
#bottomline div div { width: 732px; height: 20px; background: #000 }

Здесь corn_?? — это файлы уголков (top, left, bottom, right сокращаются по первым буквам соответственно) . Также возможно попытаться оптимизировать код, объединив некоторые div (например, вместо второго div в topline можно использовать сам #block), а также уменьшить количество обращений к серверу за счет объединения нескольких файлов с уголками в один.

Блок, видимый только при включенном JavaScript

6 августа 2008

Иногда бывает нужно сделать блок, который будет виден только в том случае, если у пользователя включен JavaScript. Самый простой способ сделать это выглядит так:

<noscript><div style="display:none"></noscript>
Здесь размещается часть, которая должна быть видна только при включенным JavaScript.
<noscript></div></noscript>

Rambler's Top100