Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #75, ФЕВРАЛЬ 2007 г.

противоукольный костюм

БОРИС ВОЛЬФСОН

Хакер, номер #075, стр. 026


BORISVOLFSON@GMAIL.COM HTTP://SPLENDOT.COM

ПРОВЕРКА ВХОДНЫХ ДАННЫХ НА PHP С ПОМОЩЬЮ ООП

ПРОБЛЕМА СОЗДАНИЯ БЕЗОПАСНЫХ СКРИПТОВ ВСЕГДА СТОЯЛА ДОВОЛЬНО ОСТРО. ДАЖЕ ЕСЛИ МЫ СОЗДАЕМ ОБЫЧНУЮ ДОМАШНЮЮ СТРАНИЦУ, НАМ ВРЯД ЛИ БУДЕТ ПРИЯТНО, ЧТО КАКОЙ-НИБУДЬ УМЕЛЕЦ ЕЕ ВЗЛОМАЕТ. ЧТО УЖ ГОВОРИТЬ О КРУПНЫХ КОРПОРАТИВНЫХ И КОММЕРЧЕСКИХ САЙТАХ, ВЕДЬ В ДАННОМ СЛУЧАЕ ВЗЛОМ МОЖЕТ НАНЕСТИ ДОВОЛЬНО ЗНАЧИТЕЛЬНЫЙ ФИНАНСОВЫЙ УЩЕРБ

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

[типичные дыры.]

Какой код может быть уязвимым к атакам взломщиков? Как взломщики пользуются дырами в скриптах? Прежде всего, отмечу, что грамотно написанный скрипт довольно трудно взломать, а большинство взломов происходит из-за недостаточной бдительности программистов. В результате такой сайт может сломать не только суперхакер Вася, но и обычная домохозяйка Вера Васильевна из соседней квартиры. «Проверять, проверять и еще раз проверять входные данные» - вот лозунг написания надежных и безопасных скриптов.

[имена файлов.]

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

Листинг

Название: «Дырявый» список ссылок

Язык: HTML

<a href="show.php?filename=article1.html">Статья 1</a>

<a href="show.php?filename=article2.html">Статья 2</a>

<a href="show.php?filename=article3.html">Статья 3</a>

Каждая ссылка указывает на скрипт show.php, который выводит статьи на экран (в качестве параметра ему передается имя файла). Сам скрипт show.php содержит следующий код:

Листинг

Название: Потенциальная дыра

// Вывод верхней части страницы

...

echo file_get_contents($filename);

...

// Вывод нижней части страницы

Конечно, мы рассмотрели самый запущенный случай: в данном коде содержатся две грубых, с точки зрения написания безопасного кода, ошибки. Во-первых, для получения внешних данных (имени файла) используется глобальная переменная $filename. Вместо этого лучше использовать ассоциативные массивы $_REQUEST, $_POST, $_GET и другие. Тем более что в следующих версиях PHP глобальные переменные на основе данных пользователя создаваться не будут, и хочешь ты этого или нет – придется использовать специальные массивы. Во-вторых, $filename никак не проверяется. Например, взломщик может указать вместо имени файла в стоке ввода адреса index.php и получит текст скрипта, а такой ценной информацией он без труда воспользуется для дальнейшего поиска недочетов в нашей системе безопасности. Следующим шагом взломщика может быть попытка вывести на экран какие-либо конфигурационные файлы с логином и паролем администратора сайта.

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

[функция system().]

Встречаются и более патологические случаи: некоторые «программисты» используют внешние данные в функции system(), которая выполняет произвольную команду операционной системы. В таком случае до дефейса сайта - буквально один шаг. Функцию system вообще не следует использовать без экстренной необходимости! Если уж пришлось задействовать ее вместе с внешними данными, то тут надо будет осуществить очень строгую проверку на спецсимволы: от всех специальных символов *nix-систем до символа с кодом 0, иначе взломщик получит возможность исполнять произвольные команды операционной системы на сервере, и от сайта останутся одни потроха.

[базы данных.]

Все реже и реже для хранения данных используются файлы, которые в последнее время почти полностью заменены базами данных. В связке с PHP обычно применяют базу данных MySQL (реже - PostgreSQL). Доступ к данным в таком случае осуществляется при помощи языка запросов SQL. В такие запросы часто приходится вставлять внешние данные, например, для гостевой книги в базе данных надо сохранить, как минимум, ник и сообщение пользователя. Язык SQL очень гибок, и поэтому здесь надо быть внимательным к входным данным. Кроме «вредоносных» данных следует обрабатывать и неправильный ввод ника пользователя, например, слишком длинную строку.

[чаты, гостевые книги, форумы.]

В данном случае нужно волноваться также и о взломе другого типа: взломщик может попытаться воспользоваться тэгами HTML, поэтому вообще нельзя допускать ввода символов «<» и «>». Их надо либо экранировать, либо выводить сообщение о неправильном вводе. Несоблюдение этого простого правила открывает широкую дорогу злому хакеру.

[решение проблемы.]

Чтобы решить эту проблему, можно собственноручно написать функции для различных проверок, но это не есть прогрессивный подход! Вспомним, что PHP5 сам по себе довольно неплохо поддерживает объектно-ориентированное программирование. Вот это – более мощный и гибкий подход, на который мы и обратим свое внимание.

Итак, пусть мы решили, что в первую очередь будем проверять размер строки. Можно написать соответствующий класс. Далее нам приходит в голову, что неплохо бы сделать проверку на отсутствие слешей и обратных слешей. Мысль опытного программиста на этом не остановится, он придумает другие проверки. Как же нам их все учитывать? Нам ведь будет нужно применять эти проверки в произвольном порядке.

Для начала - определимся предельно четко с тем, чего нам нужно достичь.

В первую очередь, нам нужно динамически добавлять новое поведение объекту, то есть различного рода верки. Можно, конечно, придумать решение самому, но у программистов есть поговорка: «Не надо изобретать велосипед». Лучше воспользоваться уже готовым решением - шаблоном проектирования, который используют профессиональные программисты с огромным опытом. В книге «Банды четырех» (Gang of Four, GoF) есть подходящий нам шаблон проектирования - Decorator. «Decorator» переводится как «украшатель», то есть мы декорируем нашу проверку длины строки дополнительными проверками. Приведу схему, по которой мы будем дальше действовать:

Component (Checker) - это просто абстрактный класс проверки, ConcreteComponent (StringChecker) - это класс для проверки длины строки, который мы будем декорировать дополнительным поведением. Decorator (Decorator) - это абстрактный класс, который нужен для организации цепочки декораторов. ConcreteDecorator (SlashChecker, BackSlashChecker) - это классы с дополнительным поведением (в нашем случае это проверки). Теперь приведу код:

Листинг

<?php

// класс проверки

abstract class Checker

{

// проверка строки $StringToCheck

abstract public function Check($StringToCheck);

// перевод «успешности» проверки в строку

// $IsOK == True - проверка пройдена,

// $IsOK == False - проверка не пройдена

function Result($IsOK)

{

if ($IsOK)

return "<font color='green'>Проверка пройдена</font><br>";

else

return "<font color='red'>Проверка не пройдена</font><br>";

}

}

// проверка размера для строк

class StringChecker extends Checker

{

function Check($StringToCheck)

{

echo "Проверка размера строки: ";

echo $this->Result(strlen($StringToCheck) <= 100);

}

}

// декоратор для класса StringChecker

abstract class Decorator extends Checker

{

// переменная для построения цепочки декораторов

private $MyChecker = null;

// конструктор запоминает следующий декоратор

function __construct(Checker $MyChecker)

{

$this->MyChecker = $MyChecker;

}

// вызывается функция проверки $MyChecker

public function Check($StringToCheck)

{

if ($this->MyChecker != null)

$this->MyChecker->Check($StringToCheck);

}

}

// проверки строки на слеши

class SlashChecker extends Decorator

{

public function Check($StringToCheck)

{

echo "Проверка строки на слеши: ";

echo $this->Result(!strstr($StringToCheck, '/'));

parent::Check($StringToCheck);

}

}

// проверки строки на обратные слаши

class BackSlashChecker extends Decorator

{

public function Check($StringToCheck)

{

echo "Проверка строки на обратные слеши: ";

echo $this->Result(!strstr($StringToCheck, '\\'));

parent::Check($StringToCheck);

}

}

// проверка строки на символы, отличные от цифр, т.е.

// строка должна содержать одни цифры

class DigitsChecker extends Decorator

{

public function Check($StringToCheck)

{

echo "Проверка строки на символы, отличные от цифр: ";

$IsOK = True;

for ($i = 0; $i < strlen($StringToCheck); $i++)

{

if ( ($StringToCheck{$i} < '0') || ($StringToCheck{$i} > '9') )

{

$IsOK = False;

}

}

echo $this->Result($IsOK);

parent::Check($StringToCheck);

}

}

$S1 = "Строка /для/ проверки";

echo "<b>Проверка строки: '$S1'</b><br>";

$Checker1 = new BackSlashChecker(new SlashChecker(new StringChecker()));

$Checker1->Check($S1);

echo "<br>";

$S2 = "1365m434\\";

echo "<b>Проверка строки: '$S2'</b><br>";

$Checker2 = new DigitsChecker(new BackSlashChecker(new StringChecker()));

$Checker2->Check($S2);

?>

Этот код должен выдавать следующий результат:

Листинг

ТИП: Результат работы программы

Проверка строки: 'Строка /для/ проверки'

Проверка строки на обратные слеши: Проверка пройдена

Проверка строки на слеши: Проверка не пройдена

Проверка размера строки: Проверка пройдена

Проверка строки: '1365m434\'

Проверка строки на символы, отличные от цифр: Проверка не пройдена

Проверка строки на обратные слеши: Проверка не пройдена

Проверка размера строки: Проверка пройдена

Класс Checker представляет собой абстрактный класс, то есть в нем объявлены специальные функции для проверки, но использовать мы их не сможем, так как они не реализованы. Например, объявлена функция Check для проверки строки. В классе StringChecker функция Check уже реализована для проверки размера строки, а абстрактном классе Decorator добавлена переменная MyChecker для организации цепочки декораторов.

Функция __construct(Checker $MyChecker) - это конструктор, который вызывается при создании объекта класса. Конструктору передается объект Checker, который будет сохранен для вызова функции Check. Функция Check класса Decorator просто вызывает $MyChecker->Check($StringToCheck). В классах для конкретных проверок нужно переписать функцию Check и не забыть в конце ее вызвать parent::Check($StringToCheck) (Check класса Decorator). Посмотрим, как эта система работает. Необходимо создать объект, который будет выполнять проверку (допустим, нам надо проверить размер строки, то, состоит ли эта строка из одних цифр и есть ли в ней слеши):

Листинг

$Checker = new DigitsChecker(new BackSlashChecker(new StringChecker()));

Теперь просто вызовем метод Check:

Листинг

$Checker->Check($S); // $S - строка для проверки

Проследим цепочку вызовов. Объект $Checker является объектом класса DigitsChecker, поэтому сначала будет произведена проверка на наличие символов, отличных от цифр. Но в конце этого метода стоит строка, которая вызывает родительский метод Check. Метод Check класса Decorator вызовет BackSlashChecker::Check, а он, в свою очередь, вызовет StringChecker::Check, на чем проверка и закончится.

[делаем выводы.]

Безусловно, можно было бы попытаться реализовать то же самое при помощи функций, но решение получилось бы не настолько гибким. С помощью же классов можно создать целую иерархию «проверяльщиков», комбинируя их в произвольной последовательности. Применение шаблона проектирования Decorator позволило создать достаточно гибкую и надежную систему проверки. Для создания нового типа проверки достаточно просто создать новый класс от класса Decorator и переопределить метод Check. Если необходимо, чтобы последовательность проверок была обратной, то есть первой выполнялась StringChecker::Check, надо просто поместить вызов метода Decorator::Check($StringToCheck) в начале.

ТИП: WWW

http://www.phppatterns.com/ - сайт содержит описания применений шаблонов проектирования в PHP 5

http://www.smartwebby.com/PHP/datevalidation.asp - неплохая статья с интерактивными примерами проверки входных данных

http://php.resourceindex.com/Functions_and_Classes/Code_Development/Data_Validation/ - обширная библиотека проверки входных данных на PHP

ООП в PHP5

[Введение.]

Достаточно часто для разработки сайтов используется язык PHP. В последней (пятой) версии PHP значительно улучшилась поддержка ООП (объектно-ориентированного программирования). Тем не менее, многие программисты при создании сайтов используют лишь самые примитивные возможности ООП, например, инкапсуляцию данных. Безусловно, такое применение ООП делает код более качественным, но, применяя и другие возможности ООП, можно добиться большего эффекта. Применение полиморфизма и наследования позволяет значительно сократить код, одновременно делая его более надежным. Также такой код можно часто использовать повторно. Рассмотрим это дело на практике. Итак, перед нами стоит задача – сделать страницу Васи Пупкина. Вверху страницы должна быть большая надпись «Домашняя страница Васи Пупкина» (обычно это логотип сайта). Далее следует меню, состоящее из следующих разделов: «Главная страница», «Биография», «Ссылки». По середине страницы идет текст раздела. Внизу дублируется меню. Сайт будет состоять из четырех основных файлов:

index.php Главная страница;

bio.php Страница с биографией Васи Пупкина;

links.php Страница с ссылками;

html.php Вспомогательный файл.

[каркас сайта.]

Наша страница будет являться классом. Определим абстрактный класс HTML-страницы в файле html.php:

Листинг

Название: Абстрактный базовый класс «страница HTML»

<?php

abstract class HTMLPage

{

protected $Title = "";

function __construct($Title)

{

$this->Title = "[Домашняя страница Васи Пупкина] " . $Title;

}

function BeginHTML()

{

echo <<<HTML

<html>

<head>

<title>{$this->Title}</title>

</head>

<body>

HTML;

}

function EndHTML()

{

echo <<<HTML

</body>

</html>

HTML;

}

function Logo()

{

echo "<h1>Домашняя страница Васи Пупкина</h2>";

}

function Menu()

{

echo <<<HTML

<table>

<tr>

<td><a href='index.php'>Главная страница</a></td>

<td><a href='bio.php'>Биография</a></td>

<td><a href='links.php'>Ссылки</a></td>

</tr>

</table>

HTML;

}

abstract function MainText();

function Write()

{

$this->BeginHTML();

$this->Logo();

$this->Menu();

$this->MainText();

$this->Menu();

$this->EndHTML();

}

}

?>

Посмотрим, для чего нужен каждый из методов:

function __construct($Title) Создание и инициализация объекта (в нашем случае - установка названия страницы);

function BeginHTML() Вывод заголовка html-файла;

function EndHTML() Вывод окончания html-файла;

function Logo() Вывод логотипа сайта;

function Menu() Вывод главного меню сайта;

abstract function MainText() Вывод основного содержания веб-страницы;

function Write() Вывод веб-страницы путем вывода ее отдельных элементов.

Часть методов служит для вывода отдельных элементов страницы, таких как меню, логотип и так далее. В методе Write все эти функции вызываются для того, чтобы вывести всю страницу целиком. Особое внимание следует уделить абстрактному методу MainText. Этот метод называется абстрактным, поскольку он не реализован в этом классе, а только объявлен. Переопределен и реализован он будет в дочерних классах. Так на странице ссылок в этом методе будут выводиться ссылки, а на странице биографии - текст биографии Васи Пупкина. Сам класс объявлен абстрактным, и, значит, создать экземпляры такого класса будет невозможно.

В классе объявлена переменная $Title с областью видимости protected, то есть доступ к ней может получить либо сам класс, либо его наследники. Теперь осталось создать остальные три файла. Покажу, как это можно сделать на примере index.php:

Листинг

Название: Главная страница сайта

<?php

include_once("html.php");

class IndexPage extends HTMLPage

{

function MainText()

{

echo "<p>Добро пожаловать на домашнюю страничку Васи Пупкина";

}

}

$Page = new IndexPage("Главная страница");

$Page->Write();

?>

В данном случае просто создается новый класс IndexPage, производный от класса HTMLPage и переопределяется метод MainText для вывода основного содержания страницы. Для наглядности приведу схему иерархии классов:

[результат.]

Преимущества использования ООП будут тем больше, чем больше будет сайт. К тому же, по ходу работы, требования к сайту будут меняться. Например, может потребоваться добавить новую страницу. Для этого надо будет лишь создать новый файл с классом, производным от HTMLPage, переопределить метод MainText и создать соответствующий пункт меню. Вот как можно использовать наследование. Также просто будет изменить дизайн всех страниц - все изменения будут происходить в классе HTMLPage, другие страницы унаследуют дизайн автоматически. Описанный подход хорошо подходит для небольших сайтов с несколькими десятками страниц.

НИКОГДА НЕ ДОВЕРЯЙ ДАННЫМ, ПОЛУЧЕННЫМ ОТ ПОЛЬЗОВАТЕЛЯ :)

ЕСЛИ ИСПОЛЬЗУЕТСЯ CMS ИЛИ ФРЕЙМВОРК, ТО В НЕМ, СКОРЕЕ ВСЕГО, УЖЕ ЕСТЬ МЕХАНИЗМЫ ПРОВЕРКИ ВХОДНЫХ ДАННЫХ

С ПОМОЩЬЮ МОДУЛЯ MOD_SECURITY ДЛЯ ВЕБ-СЕРВЕРА APACHE МОЖНО АВТОМАТИЧЕСКИ ПРОВЕРЯТЬ ВХОДНЫЕ ДАННЫЕ

ДОБАВЬ ДИРЕКТИВУ MAGIC_QUOTES_GPC = ОN В ФАЙЛ PHP.INI ДЛЯ АВТОМАТИЧЕСКОГО ЭКРАНИРОВАНИЯ КАВЫЧЕК ВХОДЯЩИХ ДАННЫХ

Содержание