Архив ‘PHP’

Определение типа загруженной картинки

Понедельник, 15 ноября, 2010

При загрузке пользователем графического файла часто возникает необходимость определить, в каком формате этот файл, чтобы применить соответствующую функцию: imagecreatefrompng, imagecreatefromjpg и т.д. Чаще всего это реализуют, проверяя расширение файла, однако это ненадежный подход (пользователь может поменять расширение файла и тем самым спровоцировать ошибки). Другой вариант — считать файл в строку и сделать createimagefromstring, однако такой подход приводит к существенному увеличению объема требуемой для обработки изображения памяти.

Наиболее правильным решением является использование функции getimagesize, которая принимает в качестве параметра имя файла (а не ресурс с изображением, как многие другие) и возвращает массив из 7 элементов. Во втором элементе этого массива указывается тип изображения (его следует сравнивать с константами IMG_GIF, IMG_JPGIMG_PNGIMG_WBMPIMG_XPM).

Полезной особенностью данной функции является то, что она работает даже при отсутствии расширения gd.

Обработка текста с тегами pre и code

Суббота, 4 апреля, 2009

Иногда требуется выполнить какую-либо обработку HTML-кода (например, расставить графические смайлики, выполнить типографирование, убрать лишние пробелы). До тех пор, пока в этом коде нет тегов <pre> и <code>, все очень просто решается обычными регулярными выражениями. Однако при наличии этих тегов возникает необходимость сохранить текст внутри них без каких-либо изменений.
В этом случае подойдет следующее решение: запомнить и заменить содержимое этих тегов специальными последовательностями, у которых очень мала вероятность встретиться в тексте, провести обработку оставшейся части, после чего провести обратную замену.
Последовательность, на которую производится замена, должна выбираться так, чтобы при обработке она никаким образом не была изменена.
Пример такого кода:
Далее…

Статус 304 и буферизация в PHP

Пятница, 30 января, 2009

Если используется буферизация средствами PHP (т.е. в скрипте имеется конструкция вида ob_start), то при выдаче статуса 304 возникает следующая ситуация: после выдачи статуса происходит выдача тела документа, содержащегося в буфере (даже в том случае, если функция ob_start была вызвана, а какого-либо вывода не производилось, т.е. буфер имеет нулевую длину). Это является нарушением протокола HTTP, согласно которому после выдачи статуса 304 контента выдаваться не должно и вызывает негативную реакцию роботов поисковых систем (в частности, Яндекс такую ошибку интерпретирует как “Передано неверное количество данных” и может удалить соответствующие страницы из своего индекса).
Избежать этой ситуации можно следующим образом: после вызова функции header с выдачей заголовка очищать буфер вывода с помощью функции ob_end_clean.

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

Пятница, 24 октября, 2008

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

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

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

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

Уязвимость через разрешенные теги или Зачем различать 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=случайный_ключ, при этом ключ хранится в сессии модератора и никому, кроме него неизвестен. При попытке выполнить удаление без ключа (или с неправильным ключом) действие просто не будет выполнено, в результате чего атака злоумышленника провалится.

Простая защита от 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, но в этом случае необходимо будет регулярно проводить оптимизацию таблицы.

Сортировка массива хешей по одному из полей

Понедельник, 4 августа, 2008

Часто требуется отсортировать массив хешей (ассоциативных массивов) по одному из полей. Обычно это делается это с помощью встроенной PHP-фунции usort и специальной callback-функции. Но вместо отдельной callback_функции можно использовать анонимную функцию, созданную с помощью create_function. Пример:

$array=usort($array,create_function('$a,$b','return strcmp($a["field1"],$b["field1"])')); // строкое сравнение двух полей
$array=usort($array,create_function('$a,$b','return $a["field2"]-$b["field2"])')); // числовое сравнение

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

Работа с формами

Суббота, 26 июля, 2008

Работа с формами — одна из самых типинчых задач при Web-программировании. Чаще всего в форме требуется либо ввести новую информацию о каком-либо объекте, либо отредактировать уже существующую, причем для нового объекта есть значения полей по умолчанию. Существует много разных способов фомирования форм, но наиболее красивым решением является следующий: форма хранится в обычном HTML, в поля прописаны значения по умолчанию. Перед выводом формы в броузер скрипт формирует хеш со значениями полей, которые нужно показать пользователю, после чего вызывает функцию, которая парсит форму и заменяет значения по умолчанию на значения из этого хеша (имя поля  в хеше должно совпадать с именем поля в форме).

Вот вариант реализации такой процедуры (параметры: первый параметр — html-код с формой, второй параметр — хеш с данными, третий параметр — хеш с дополнительными опциями для select’ов, которые будут дописаны к уже существующим в статике в процессе парсинга selectа, ключ соответствует имени select-поля, значение — хешу вида имя_опции=>значение_опции):

/** Функция установки значений полей форм в соответствии с параметрами в хеш-массиве data.
(Ключи массива должны соответствовать именам полей.)
Если ключ в массиве отсутствует, поле остается без изменений
(таким образом, имеет смысл прописывать в качестве значений полей значения по умолчанию).
**/
function cmsmain_html_set($html,$data,$params=false) {
  $count = preg_match_all('|(<input[^>]+?>)|is',$html,$inputs);
  for ($i=0; $i<$count; $i++) {
    $name=_cmsmain_html_getparam($inputs[0][$i],'name');
    if (($newvalue=_cmsmain_html_data($data,$name))!==false) {
      $newstr=false;
      $value=_cmsmain_html_getparam($inputs[0][$i],'value');
      $type=_cmsmain_html_getparam($inputs[0][$i],'type');
      if ($type=="radio" || $type=="checkbox") {
        if ($value==$newvalue) {
          $newstr=$inputs[0][$i];
          if (strrpos($newstr,'/>')===strlen($newstr)-2) $newstr=str_replace('/>',' checked="checked"/>',$newstr);
          else $newstr=str_replace('>',' checked>',$newstr);
        }
	      else {
	        $newstr=preg_replace('|\s+checked(=\S+?)?([\s/>])|s','$2',$inputs[0][$i]);
	      }
      }
      else {
        if (preg_match('|\s+value=[\'"]?'.$value.'[\'"]?([\s/>])|is',$inputs[0][$i])) {
          $newstr=preg_replace('|\s+value=[\'"]?'.$value.'[\'"]?([\s/>])|is',' value="'.htmlspecialchars($data[$name]).'"$1',$inputs[0][$i]);
        }
        else $newstr=preg_replace('|(/?>)$|s',' value="'.$newvalue.'"$1',$inputs[0][$i]);
      }
      if ($newstr) $html=str_replace($inputs[0][$i],$newstr,$html);
    }
  }
  $count = preg_match_all('|(<textarea[^>]+?>)(.*?)</textarea>|is',$html,$textareas);
  for ($i=0; $i<$count; $i++) {
    $name=_cmsmain_html_getparam($textareas[1][$i],'name');
    if (($newvalue=_cmsmain_html_data($data,$name))!==false) {
      $newstr=$textareas[1][$i].$newvalue.'</textarea>';
      $html=str_replace($textareas[0][$i],$newstr,$html);
    }
  }
  $count = preg_match_all('|(<select[^>]+?>)(.*?)</select>|is',$html,$selects);
  for ($i=0; $i<$count; $i++) {
    $name=_cmsmain_html_getparam($selects[1][$i],'name');
    if (isset($params[$name]) && is_array($params[$name])) {
      $newselects = '';
      foreach ($params[$name] as $key=>$value) {
        if ($value=='') $value=$key;
        $newselects.='<option value="'.htmlspecialchars($key).'">'.htmlspecialchars($value).'</option>';
      }
      $newbigstr=str_replace($selects[2][$i],$selects[2][$i].$newselects,$selects[0][$i]);
      $selects[2][$i].=$newselects;
    }
    else $newbigstr=$selects[0][$i];
    if (($newvalue=_cmsmain_html_data($data,$name))!==false) {
      $optcount = preg_match_all('|<option([^>]+?)>(.*?)<|is',$selects[2][$i].'<',$options);
      for ($j=0; $j<$optcount; $j++) {
        $value=_cmsmain_html_getparam($options[1][$j],'value');
        if ($value==$newvalue) {
          $newstr=$options[0][$j];
          $newstr=preg_replace('|(<option[^>]+?)>|s','$1 selected="selected">',$newstr);
        }
	      else $newstr=preg_replace('|\s+selected(=\S+?)?([\s/>])|s','$2',$options[0][$j]);
        $newbigstr=str_replace($options[0][$j],$newstr,$newbigstr);
      }
      $html=str_replace($selects[0][$i],$newbigstr,$html);
    }
  }
  return $html;
}

/** Извлечение параметра из html-тега. **/
function _cmsmain_html_getparam($input,$param) {
  if (!preg_match('|\s+'.$param.'="(.*?)"|is',$input,$matches)) {
    if (!preg_match('|\s+'.$param.'=\'(.*?)\'|is',$input,$matches)) {
      if (!preg_match('|\s+'.$param.'=(\S+?)|is',$input,$matches)) { $matches[1]=''; _dbg('Fuck!'); }
    }
  }
  return $matches[1];
}

/** Извлекает данные из массива и применяет к ним htmlspecialchars.
На данный момент не поддерживается обработка спецсимволов (типа [] для вложенных массивов)
**/
function _cmsmain_html_data($data,$name) {
  if (isset($data[$name])) return htmlspecialchars($data[$name]);
  else return false;
}

Код обновлен 4 августа 2008 года.

Параметры-массивы и параметры-строки

Пятница, 20 июня, 2008

Бывают ситуации, когда требуется выполнять одинаковую обработку данных, вынесенную в функцию, для строк с определенным разделителем и для массивов данных. Обычно в таком случае строка преобразуется в массив функцией explode перед вызовом функции, выполняющей обработку. Однако если такое случается часто, то есть другой способ: выполнять преобразование строки в массив (или массива в строку) непосредственно в самой функции. Делается это с помощью проверки типа аргумента функции с помощью is_string или is_array.

Пример такой ситуации:

function proc($data) {
  if (!is_array($data)) $data=explode(';',$data);
  foreach ($data as $curdata) {
      // обработка текущего элемента $curdata
  }
} 

$a="aaa;bbb;ccc";
$b=array("aaa","bbb","ccc");
proc($a);
proc($b); // результаты вызовов функции proc для $a и $b будут полностью одинаковыми

HURL и относительные пути

Пятница, 14 марта, 2008

При разработке CMS, использующих HURL, часто возникает проблема с относительными ссылками (например, в меню сайта): при переходе на следующий уровень вложенности они перестают работать (т.е. если ссылка имела вид register.php, а CMS установлена в каталог /cms/, то когда заходим на страницу с адресом /cms/section1/page.htm, то ссылка превращается в /cms/section1/register.php и перестает работать).

Решений этой проблемы может быть несколько:

1) прописывать корневой URL в теге BASE: <base href=”http://sitename.ru/cms/”>. Наиболее простой способ, но его неудобство становится очевидным, как только требуется добавить в CMS статью, скопированную с другого сайта: если в ней содержатся относительные ссылки (например, оглавление в начале статьи), они перестают работать и требуется ручная правка, причем если у статьи меняется URL, правки приходится вносить еще раз.

2) динамически корректировать все ссылки, которые должны быть относительно корня CMS в соответствии с текущим уровнем вложенности (т.е. в приведенном примере потребовалось бы приписать в начало ссылки register.php конструкцию ../). Способ весьма неудобный с точки зрения реализации.

3) указывать ссылки относительно корня сайта (т.е. /cms/register.php). У этого способа нет проблем, характерных для двух предыдущих, однако для него требуется знание пути, по которому установлена CMS.

Узнать каталог, куда установлен скрипт, можно с помощью такого выражения:

$script_url=str_replace(basename(__FILE__),'',$_SERVER['SCRIPT_NAME']);

Примечание: если скрипт, выполняющий эту операцию, лежит не в корне, а в подкталоге (например /cms/libs), то выражение несколько усложняется:

$script_url=str_replace('libs/'.basename(__FILE__),'',$_SERVER['SCRIPT_NAME']);

Представленные выражения будут работать даже при использовании mod_rewrite или HURLs вида index.php/section1/page.htm.


Rambler's Top100