Имитация файлов и директорий

Имитация файлов и директорий

Адрес вашего сайта появляется на пользовательском экране одновременно с дизайном и контентом. Поэтому адрес является полноправной частью сайта. Адрес типа www.фирма.ру (www.фирма.город.ру), естественно, гораздо лучше, чем www.geocities.com/Gonduras/San-Pedrillio/~наша_фирма, кто спорит. А вот по вопросу понятных человеку адресов внутри сайта общественность четкого консенсуса пока не нашла.

Однако пользователю приятнее было бы видеть адрес типа /services/special/ чем /content.phtml?q=e23908a234cc239b3445127.

Лирическое отступление. Помню, на Интернити-99 мне показали флэш-ролик Hewllett Packard Laser Jet 3100. Через пару недель я вспомнил про него и решил скачать его из дома. Я бы долго бродил в бесполезных поисках по сайту Лексмарк (чего вы смеетесь, это так и было!), если бы не их адреса. На HP адреса были понятные - что-то вроде “/products/printers/laserjet/3100″, а на сайте Лексмарка было вот именно это непонятное “q=492898748273″. Я был в сомнениях, но через день вспомнил-таки, что это был HP :).

Кстати, на этом сайте адреса выпусков, версий для печати и всех информационных страниц (ссылки, файлы и т.д.) виртуальные, файлов с такими названиями не существует.

Делается это достаточно просто. В файле .htaccess пишутся строчки, например
ErrorDocument 404 all.php
ErrorDocument 403 all.php
ErrorDocument 401 all.php

Файл all.php обрабатывает переменную $REQUEST_URI и, если нужная информация найдена, выдает команду
header (”HTTP/1.0 200 Ok”);

Это необходимо для того, чтобы браузер IE 4 считал, что страница найдена, а не подставлял вместо нее свою служебную вывеску “адрес не найден”. В остальных случаях, даже если запрошен адрес “all.php”, пользователю будет выдаваться сообщение о том, что файл не найден.

Если при вызове функции header сервер ругается матом “Error 500″ - смотрите здесь и здесь.

Конечно же, выдать заголовок и нарисовать страницу - нехитрое дело. Отслеживание результатов запросов, проверка на ошибки - это скорее рутина. Самое ответственное дело - разбор запрашиваемого адреса.

Тут приемов много. Например, у меня версия для печати и страница отзывов ищутся по регулярным выражениям:
if (preg_match(”/(d+)-comment/A”, $url, $res)) …

А потом из переменной $res[0] беру номер выпуска и проверяю наличие его в базе. Адрес из нескольких поддиректорий можно, например, при помощи взрыва :)
$dir = explode(”/”, $url);

и потом обрабатывать подстроки по одной (перед взрывом надо убрать из начала и конца строки слеши).

Кстати, перед тем, как писать материал, я честно отправил письмо на info@lenta.ru с вопросом, используется ли у них такой метод обработки запроса или там действительно существуют директории. Мне не ответили. Не буду гадать, как у них сделано, напишу, как бы я делал обработку адреса.
if (preg_match(”/([a-z]+)/(d{4})/(d{2})/(d{2})/([a-z]+)/A”, $url, $match)) {
$request = “SELECT news_id FROM news, rub WHERE news.rub_id=rub.rub_id AND
rub_address=’”. $match[1]. “‘ AND
news_date LIKE ‘”. $match[2]. “-”. $match[3]. “-”. $match[4]. “‘ AND
news_address=’”. $match[5]. “‘”;

Этот запрос делается просто для проверки, есть ли такая новость в базе. А потом в зависимости от результата выдается либо страница с новостью, либо какая-нибудь ругань (или главная страница рубрики/сайта).

А вот как я проверяю адреса на этом сайте:
if (preg_match(”/(d+)-print/A”, $url, $res)) {
// версия для печати
}
elseif (preg_match(”/(d+)-comment/A”, $url, $res)) {
// все отзывы
}
elseif (!preg_match(”/D/”, $url)) {
// полная версия выпуска
}
else {
// либо остальные рубрики, либо адрес не найден
};

Кстати, у себя я как честный человек выдаю header(”HTTP/1.0 200 Ok”) только если выпуск/рубрика найдены.

И еще один пример. Допустим, рубрики сайта построены как дерево, а таблица в базе выглядит так:
CREATE TABLE rubrika (
id TINYINT NOT NULL AUTO_INCREMENT,
parent_id TINYINT,
address VARCHAR(16) NOT NULL,
title VARCHAR(128) NOT NULL,
rub_text TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE address (address)
);

Что такое title и rub_text - объяснять не надо. Поле address - это адрес, по которому будет запрашиваться рубрика (новости нужно сделать рубрикой первого уровня, и в поле address будет “news”). Поле parent_id - идентификатор рубрики уровнем выше.

Теперь, если рубрики заведомо не могут быть ниже второго уровня, анализ адреса не составит особого труда.
$url = $REQUEST_URI;

// убираем слеши из начала и конца адреса
$url = ereg_replace(”^/”, “”, $url);
$url = ereg_replace(”/$”, “”, $url);
$dir = explode(”/”, $url)

// случай, когда запрошена рубрика второго уровня.
if (sizeof($dir)==2) {

// составляется запрос, объединяющий таблицу rubrika саму с собой.
//Здесь надо заметить только, что таблица first подразумевает рубрику
// второго уровня, а second, наоборот, первого.
$request = “SELECT first.id, first.title, first.rub_text FROM
rubrika first, rubrika second WHERE
first.parent_id=second.id AND first.address=’”. $dir[1]. “‘ AND
second.address=’”. $dir[0]. “‘”;

// Отправляем запрос в базу, а потом обрабатываем результат.
$result = mysql_query($request);

if (!mysql_error() && @mysql_num_rows($result)==1) {

}

// Это на случай, если запрос прошел успешно, но ничего не найдено
elseif (!mysql_error()) {

}

// …и на случай, если произошла ошибка.
else
die (”Ошибка БД. MySQL пишет: “. mysql_error());
}

// Запрошена рубрика первого уровня. тут и делать-то нечего :)
elseif (!ereg(”/”, $url)) {
$request = “SELECT id, title, rub_text FROM rubrika WHERE address=’$url’”;

};

Хватит примеров? Дальше - дело фантазии. Подведем итоги, распишем положительные и отрицательные моменты.
Плюсы
Красивые адреса, возможность зайти в рубрику, набрав ее адрес на клавиатуре. Благодарность от фанатов клавиатуры.
Уменьшение количества файлов, уменьшение количества повторяющихся операций в разных файлах.
Централизация вывода. Сбор большинства операций в единой точке входа.
Скрытие некоторой технологической части сайта.
Минусы
Увеличение ресурсоемкости за счет проверки адреса и компиляции большого файла вместо нескольких маленьких.
Сложность с введением новых параметров (я, можно сказать, удачно вывернулся с версией для печати, но было бы более логично видеть адреса типа /13/print). Кое-что придется сбрасывать, например в куки.
Кое-что, например, поиск, так и останется вне “точки входа” (хотя… “How IT works” делает поиск в адресе, но для более-менее сложного сайта это будет неудобно или невозможно).
Дополнительные сложности с адресами картинок и навигации по сайту (броузер-то мерит все адреса относительно открытого документа, пусть даже из несуществующего адреса).

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

В прошлом выпуске я описал работу с ЧПУ (”человекопонятные УРЛы”) через ошибку #404. Сам пользуюсь именно этой схемой. Но через день после публикации материала свой отзыв написал Константин Шевченко (aka cat) и предложил мне рассказать публике о других способах работы с адресами на сайте. Он же меня и консультировал.

В отзывы, так же написали, что это серьезно грузит систему. Да, грузит. И я об этом честно написал. И еще одна поправка: диалог в отзывах

Посетитель: А интересно, что скажут поисковики на этот мнимый ErrorDocument?

Я: Поисковики считают такие адреса нормальными - они делят всё на 200 Ok и 404 Not Found. А остальное им по барабану.

Поисковики, конечно же, не делят все на 200 и 404. Они знают и другие коды сервера, просто никто не может проверить на уровне программ, существует ли документ, который прислал сервер или нет. Если код 200, значит существует и доступен. И ничего более.

Потом Юра Буров написал по поводу предыдущего материала в “Еженедельках”. Однако, это не все, что можно написать, как думает он.

Одна вещь, которую забыл описать в предыдущем выпуске - виртуальный файловый архив. Через ErrorDocument вполне возможно отслеживать запросы к несуществующей директории download и выдавать запрошенные файлы из базы, а администратор сайта мог бы работать с этим архивом через веб-форму. Чтобы правильно выдавать тип файла (content-type), нужно брать его тогда, когда его закачивает администратор. В таблице файлов сделать дополнительное поле, в котором хранить эти типы, и выдавать их в заголовке. Разумеется, произойдет снижение производительности сервера.

А теперь о том, что описал Костя. Перечислю способы по возрастанию сложности (порядок, впрочем, спорный).
Сервер ищет файл с тем же именем

Оказывается, достаточно прописать в установках директории (httpd.conf или .htaccess) строку Options Multiviews или, если директива Options уже есть, добавить MultiViews к ней. Тогда если пользователь набирает “<адрес директории>/foo/bar”, сервер будет искать файл с именем “foo” и с любым расширением. Найденный с наибольшим совпадением (вот это для меня загадка) файл он обработает с его типом mime, то есть если есть news.php, а набран адрес news/, то сервер отдаст адрес на обработку php. Если это картинка, то сервер отдаст ее браузеру именно как картинку (послав соответствующий заголовок content-type). А в news.php разбираем $REQUEST_URI. Например, если новости выводятся целой лентой, либо за определенную дату, разбор можно сделать таким:
/* Первый вариант - когда набран адрес типа “/news/010120″, возможно с
// дробью на конце. Символы ^ и $ здесь обозначают привязку к началу и
// концу строки. Подстрока [0-9]{6} означает 6 цифр (если у вас новости
// могут быть датированы 1999-м годом и раньше, используйте адреса с
// полным форматом года и 8 цифр вместо 6). */
if (ereg(”^/news/([0-9]{6})$”, $REQUEST_URI, $match) ||
ereg(”^/news/([0-9]{6})/$”, $REQUEST_URI, $match)) {

}

/* второй вариант - набран адрес просто “/news” или “/news/” */
elseif (ereg(”^/news/$”, $REQUEST_URI) || ereg(”^/news$”, $REQUEST_URI)) {

}

/* запросы ко всем остальным адресам (в этом файле) считаются попытками
// взлома сайта */
else
die (”Error 404 Not found”);

То же самое можно сделать, например, с каталогом продукции фирмы - вынести все это дело в отдельный файл catalogue.php, а адреса сделать вида “/catalogue/rubrik1/rubrik2/rubrik3″. При этом в файле catalogue.php начало строки будет откусываться, а дальше можно обработать по принципу, описанному в предыдущем выпуске. Остальное же можно отправить, опять же, в ErrorDocument.
Сервер разбирает запрос

Метод похожий, но на вскидку менее ресурсоемкий, потому что не приходится искать файлы по директории.

В установках директории (опять же httpd.conf или .htaccess) пишем:
<FilesMatch “^(news)$”>
ForceType application/x-httpd-php
</FilesMatch>

В директории лежит файл с именем “news” (именно “news”, без расширения). Когда запрашивается адрес “/news”, либо “/news/bla-bla-bla”, сервер выполняет файл news как php-скрипт. А внутри него производится обработка переменной $REQUEST_URI.

Чтобы не писать для каждого подобного файла свой блок FilesMatch, нужно немного изменить строку шаблона. Пусть сервер ищет файлы без расширения, то есть те, у которых в имени нет точки:
<FilesMatch “^([^\.]+)$”>

Очень удобно! Когда-нибудь поставлю такое же и себе.
Сервер переписывает запросы

Очень полезная вещь mod_rewrite. Ею можно сделать все вышеописанное, и много другого.

К сожалению, моя битва с mod_rewrite не увенчалась успехом (Костя пишет, что нижеописанного достаточно для работы Rewrite Engine под Unix. У меня под win98 - ни в какую…). Поэтому описываю очевидные вещи и то, что описал Костя.

Для начала надо раскомментировать строку
LoadModule mod_rewrite <путь к модулю/имя файла>

в httpd.conf. В конфигурации директории пишем строку “RewriteEngine On”. Затем - команду RewriteRule: RewriteRule <шаблон> <замена>

Например RewriteRule ^(.*).html$ /otherdir/$1.html (все без кавычек). Вот, собственно, и все. Все, что я так и не смог проверить : Я спросил у ясеня, я спросил у тополя, я спросил у форума… форум не ответил мне. (мелодично) Я спрошу у публики… На всякий случай, спрашиваю у уважаемой публики: как запустить Rewrite Engine под win98 se + apache/1.3.14 + php/4.0.4-Antonio (установлен как модуль) ?

А пока еще один пример (опять же от Шевченко):
RewriteEngine On
RewriteRule ^(.*).htm$ /portal/$1

<FilesMatch “(portal)$”>
ForceType application/x-httpd-php
</FilesMatch>

Там лежит один файл с именем “portal”, в который перенаправляются все запросы к html-файлам в данной директории. И получается, как будто там лежат файлы.

Напоследок вспомним Ленту.ру.

Вот здесь, в са-а-амом конце Носик говорит про ресурсоемкость их технологии. “Издательская машинка, написанная Максимом Евгеньевичем Мошковым, (который библиотека) интересна тем, что она весит около 60Kb”

Не знаю, что из себя представляет эта система (скорее всего, скомпилированный ), гадать не буду. Мне хотелось бы прикинуть, как динамическую адресацию можно реализовать через php. Впрочем, тут не в нем дело - был бы Апач. Итак, схема адресов <рубрика>/<год>/<месяц>/<день>/<новость>. Если отрезать имя новости, получим материалы рубрики за день. Если отрезать день, месяц и год, получим последние материалы рубрики.
RewriteRule ^([a-z]+)/$ rubika_last/$1
RewriteRule ^([a-z]+)/([0-9]{4})/([0-9]{2})/([0-9]{2})/$ rubrika_date/$1/$2-$3-$4
RewriteRule ^([a-z]+)/([0-9]{4})/([0-9]{2})/([0-9]{2})/([a-z]+)/$ rubrika_news/$1/$2-$3-$4/$5

<FilesMatch “^rubrika”>
ForceType application/x-httpd-php
</FilesMatch>

Преимущества этого метода по сравнению с единой точкой входа EerrorDocument очевидны: движок php интерпретирует только то, что будет выполняться. Никаких switch/case или if/elseif/else, никаких лишних строк.

Все остальное - отдаем ErrorDocument “как в предыдущей задаче”.

Источник http://phpclub.ru/

Tags: mod_rewrite, php, файл

Добавить комментарий Март 19, 2008

Защита сайта от SQL инъекции с помощью mod_rewrite

Не секрет, что можно взломать абсолютно любой сайт, идеальной защиты не бывает. Взломать интернет сайт, намного легче, чем взломать прикладную программу, да и делать это гораздо интереснее, ведь твои труды увидят тысячи пользователей (разумеется если взломанный сайт достаточно популярен). На сегодняшний день известно множество методик взломов интернет-сайтов, одним из самых опасных является так называемая инъекция ( Injection - введение). Инъекция бывает разной можно внедрить свой код в программу, можно внедрить инородные данные в поток чужих данных, но одной из самых опасных для интернет-сайтов является SQL инъекция ( SQL Injection ). Из названия уже становится понятно, что и куда внедряется. На всякий случай поясню, при SQL инъекции мы внедряем наш код в SQL запрос, в результате чего при благополучных обстоятельствах мы можем получить данные хранящиеся в базе данных не доступные для просмотра стандартными средствами. Многие полагают, что SQL инъекцией страдает только база данных MySQL, но это конечно же не так. SQL инъекцию можно осуществить в любую базу, поддерживающую языки запросов (а таких большинство).

Рассмотрим вышесказанное на простом примере. Рассматривать все будем на примере MySQL, так как эта БД является одной из самых популярных среди web-программистов.

Пусть у нас есть таблица в которой есть 3 поля (md,password,login). Пусть у каждого пользователя есть свой секретный код (md), например он может быть такой sdf897sdsdf87sdf99sdf87. В поле password записан пароль пользователя, а в поле login имя пользователя.

Пусть у нас есть следующий код на PHP :
$result = mysql_query ( “SELECT password FROM user_table WHERE md=?$usermd?” ); // сам запрос
$pass = mysql_result ( $result , ?? , ?password? ); //записываем в переменную $pass значение поля password
echo $pass ; // выводим переменную $pass на экран

Этот код берет из таблицы user_table значение поля password у записи, md которой равен переменной $usermd, а затем выводит этот пароль. Пусть каждый пользователь знает свой собственный md, но не знает md других пользователей, тогда очевидно, что попытка подобрать md чужого пользователя на 99% обречена на провал.

Как же можно узнать пароль другого пользователя? Оказывается можно, достаточно поместить в переменную $usermd свой код, например такой ” ? or login=?admin?# ” (подразумевается что, администратор имеет login=”admin” ). Как будет Хакер делать подмену, это вопрос другой, но факт остается фактом, в данном случае на экран выведется пароль администратора.

Давайте проанализируем, что же произошло. В результате подмены запрос принял вид:

“SELECT password FROM user_table WHERE md=?? or login=?admin?#?”

Думаю что теперь все понятно, поясню лишь, зачем нужен символ #. Все что находится после символа # обрезается, т.е. не является частью запроса, сам символ # является спецсимволом и разумеется тоже не участвует в запросе. При таком запросе база попытается выбрать запись с пустым полем md, или у которых login=?admin?, поскольку записей с пустым md не существует, то выберется запись у которой поле login равно “admin”.

Это всего лишь пример, он не претендует на звание наиярчайшего примера SQL инъекции, он лишь демонстрирует, к чему все это может привести. Не стоит считать, что такого абсурдного кода не существует, и никто не будет открыто хранить пароль администратора. SQL инъекцией страдают большинство CMS а также крупнейших Forum ?ов, в том числе и InvisionPB, не говоря уже о PhpBB и его клонах.

Все это как ни странно было всего лишь вступлением, на самом деле я не планировал рассказывать о инъекции, но как говориться “нужно знать врага в лицо”. Как же можно защитить свои скрипты, от подобной уязвимости? На самом деле очень легко, нужно всего лишь проверять входные данные. Реализовать это можно различными способами, лучший из них, это просто проверить наличие спецсимволов в переменной, и если таковые имеются занести в лог информацию о попытке взлома, вместе с IP взломщика, чтобы в последствии можно было его забанить. Второй способ более простой, можно сразу не проверяя вырезать из переменной все спецсимволы, например с помощью команды str_replace. Этот способ простой, но имеет недостаток: SQL запрос все равно будет выполнен, причем переменная будет заведомо содержать ошибочные данные, поэтому на экране может отобразиться целая гора ошибок (в зависимости от сложности скрипта).

Я хочу предложить свой способ защиты скриптов. Этот способ не является лучшим, но на мой взгляд, он очень изящен и имеет несколько плюсов, о которых я конечно же расскажу. Способ заключается в использование модуля для сервера Apache, который носит название mod_rewrite. Этот модуль установлен практически на всех платных хостингах. Задача этого модуля - изменять введенную информацию пользователя в строке URL на другую, нужную вам. Чтобы было понятнее приведу простой пример.

Допустим, пользователь введет в строке адрес http://site.ru/download.html. Этот адрес получает сервер, но только после того, как его обработает mod_rewrite (если он включен конечно). Модуль mod_rewrite анализирует введенный адрес, и согласно своим настройкам изменяет или не изменяет его. Изменить он может на что угодно, например он может отдать серверу вместо введенного адреса другой адрес например http://site.ru/article.html, в результате чего у пользователя отобразиться на экране страница article.html, хотя в строке адреса по прежнему будет отображаться http://site.ru/download.html, и пользователь даже не будет подразумевать, что его “обманули”.

Зачем же нужен этот модуль? На самом деле цель у него одна - сделать ссылки более красивыми, но использовать его можно для разных целей. Одной из них и является защита сайта от SQL инъекции.

Давайте вернемся к нашему примеру с паролем администратора и попробуем защитить наш скрипт с помощью mod_rewrite. Пусть переменная $usermd передается скрипту GET запросом: http://site.ru/script.php?usermd=4k2jhk34jhkjh6kh7kh33. Как видите в переменной $usermd будет храниться значение “4k2jhk34jhkjh6kh7kh33″, мы знаем, что эта переменная может содержать цифры от 0 до 9 и латинские буквы от a до z. Теперь давайте с помощью mod_rewrite изменим наш адрес на более красивый, а заодно и реализуем проверку на входные данные.

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

RewriteEngine on
Options +FollowSymlinks
RewriteBase /
RewriteRule ^.htaccess$ - [F]

Эти команды сообщают серверу, что необходимо использовать модуль mod_rewrite. Далее за этим кодом будут следовать правила замена адресов, общий вид которых:

RewriteRule входной_адрес на_что_заменить

Например, чтобы нам подменить адрес http://site.ru/download.html на http://site.ru/article.html необходимо написать:

RewriteRule http://site.ru/download.html http://site.ru/article.html

Как видите все просто. Но на самом деле все чуточку сложнее. Дело в том, что в данном примере мы имеем дело со статическими страницами, адрес которых никогда не измениться, если же мы имеем дело со скриптами, переменные в которых принимают разные значения, такой способ не подойдет. Для этого mod_rewrite позволяет использовать регулярные выражения. Если вы с ними не знакомы, то вам будет сложно разобраться с модулем, но все же можете попытаться. Советую вам почитать дополнительную информацию о регулярных выражениях, так как она никогда лишней не бывает.

Снова вернемся к нашему примеру. Что же нам необходимо сделать? Нам нужно заменить адрес http://site.ru/script_4k2jhk34jhkjh6kh7kh33.html на адрес http://site.ru/script.php?usermd=4k2jhk34jhkjh6kh7kh33.

Делается это следующей строкой:

RewriteRule http://site.ru/script_([a-z0-9]*).html http://site.ru/script.php?usermd=$1

Думаю нужно сделать немного пояснений. Выражение ( [a-z0-9]* ) означает, что в данном месте может находиться последовательность из цифр и букв любой длины. [a-z0-9] - перечисление допустимых символов (в данном примере заданы диапазоны), знак * означает что таких символов может быть несколько. Выражение которое соответствует маске находящейся в скобках присваивается переменной $1 (цифра обозначает номер скобок) и вставляется в адрес на который будем заменять входящий адрес.

В результате этой манипуляции после того как пользователь введет в строке адреса http://site.ru/script_4k2jhk34jhkjh6kh7kh33.html он автоматически попадет на страницу http://site.ru/script.php?usermd=4k2jhk34jhkjh6kh7kh33. И конечно же переменная $usermd будет содержать значение “4k2jhk34jhkjh6kh7kh33″.

Теперь давайте рассмотрим, что же произойдет если пользователь попытается ввести http://site.ru/script_?%20or%20login=?admin?#.html ( %20 - тоже самое что и пробел). В результате этого запроса адрес попадет модулю mod_rewrite, который проанализирует его, т.к. выражение ?%20or%20login=?admin?# не подходит под маску ( [a-z0-9]* ), т.к. содержит недопустимые символы, то mod_rewrite ничего не сделает, так как будто его вообще нет, очевидно, что в этом случае пользователю будет возвращена ошибка 404 Page not found (404 страница не найдена).

Это и есть защита. Она не идеальна и имеет минусы, но есть и плюсы. Главный минус - это то, что в скрипты, все же можно передать вредоносную информацию, но для этого придется воспользоваться методом POST, а не GET. Второй минус - mod_rewrite немного нагружает сервер, как и любые регулярные выражения. Главный плюс - пользователь не видит, как называются ваши переменные.

Ну вот собственно и все, о чем я хотел рассказать. Я показал всего лишь пример, на самом деле модуль mod_rewrite намного мощнее, собственно как SQl инъекция. И помните, идеальной защиты не существует.

Данная информация дана в ознакомительных целях, автор не несет ответственности за возможно взломанные скрипты.

Нгуен Павел http://excode.ru

Tags: htaccess, mod_rewrite, MySQL, Options, SQL

Добавить комментарий Март 4, 2008


Календарь

Сентябрь 2010
Пн Вт Ср Чт Пт Сб Вс
« Апр    
 12345
6789101112
13141516171819
20212223242526
27282930  

Записи по месяцам

Записи по рубрикам

Бегун