Улучшение защиты веб-приложений с помощью среды Zend Framework 2

Веб-приложения уязвимы для атак различного характера, включая SQL-инъекции, XSS-атаки, CSRF-атаки, спам, раскрытие паролей методом грубой силы и т. д. Тем не менее написанное на PHP веб-приложение можно с легкостью защитить от большинства распространенных атак с помощью компонентов безопасности, предоставляемых средой Zend Framework 2. В данной статье демонстрируется использование этих компонентов для повышения безопасности веб-приложений с помощью таких методик, как валидация ввода в формы, фильтрация данных, вводимых ботами, отклонение спама в комментариях и регистрация необычных событий.

Защита от вредоносных программ: комплексный подход к противодействию одной из самых серьезных современных ИТ-угроз

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

Загрузите технический обзор Defending against malware.

Введение

При создании нового веб-сайта или веб-приложения разработчик не всегда может уделить достаточно времени обдумыванию вопросов безопасности. Сегодня в распоряжении злоумышленников огромное количество способов "прогрызания дыр" в целевое веб-приложение, поэтому при написании программного кода приложения необходимо неукоснительно соблюдать наилучшие методики обеспечения безопасности, чтобы гарантировать защищенность пользователей.

Если разработчик применяет язык PHP, эта задача облегчается благодаря среде Zend Framework, которая предоставляет несколько готовых к использованию компонентов для улучшения защиты создаваемого веб-приложения. В этой статье я описываю пять подобных компонентов, которые позволяют разработчику укрепить защиту своего веб-приложения посредством таких механизмов, как валидация вводимой информации, экранирование спецсимволов в выводимой информации, регистрация событий в журнале, тестирование на наличие спама и фильтрация информации от ботов.

При совместном использовании эти компоненты служат первой линией обороны вашего приложения от атак злоумышленников. Перейдем к основному материалу статьи.


Настройка базового приложения

Прежде чем переходить к программному коду, необходимо упомянуть несколько предупреждений и допущений.

На всем протяжении этой статьи предполагается, что ее читатель хорошо владеет базовыми принципами разработки приложений с использованием среды Zend Framework 2.x; понимает взаимодействие между действиями (action), контроллерами (controller) и представлениями (view), а также знаком с реализацией пространства имен в PHP 5.3.

Кроме того, предполагается, что в распоряжении читателя имеется действующая среда разработки Apache/PHP, а его веб-сервер Apache сконфигурирован для поддержки виртуального хостинга и перезаписи URL посредством htaccess-файлов. Если вы не знакомы с этими вопросами, обратитесь к дополнительной информации, ссылки на которую приведены в разделе Ресурсы данной статьи.

Zend Framework — это слабо связанная среда, поэтому ее компоненты можно использовать как на автономной основе, так и в рамках MVC-реализации этой среды. С учетом того, что различные проекты имеют различные потребности, в данной статье демонстрируется использование этой среды в обоих сценариях. Кроме того, в разделе Материалы для загрузки приведены ссылки на примеры работающего кода.

Для начала настройте стандартное приложение Zend Framework 2.x, которое будет служить контекстом для кода, демонстрируемого в этой статье. Для этого загрузите и используйте модуль ZFTool, как показано ниже.

shell> php zftool.phar create project example
shell> cd example
shell> php composer.phar install

Альтернативный подход состоит в том, чтобы в ручном режиме загрузить и извлечь содержимое скелетного приложения Zend Framework в каталог на вашей системе, а затем с помощью Composer загрузить необходимые зависимости. В документации по Zend Framework содержатся дополнительные указания по применению этого подхода; ссылка на документацию приведена в разделе Ресурсы .

Теперь в вашей конфигурации Apache следует задать новый виртуальный хост для этого приложения, например, http://example.localhost, а в корневом каталоге документов этого виртуального хоста указать каталог public/ скелетного приложения. После этого на вышеупомянутом хосте вы увидите начальную страницу среды Zend Framework 2.x по умолчанию (см. рис. 1).

Рисунок 1. Начальная страница Скелетное приложения Zend Framework 2
Zend Framework 2 skeleton application welcome page

И, наконец, воспользуйтесь инструментом ZFTool для создания нового контроллера (controller) в модуле Application. Этот контроллер содержит примеры кода для данной статьи.

shell> php zftool.phar create controller Example Application example/

Теперь, когда все базовые условия соблюдены, перейдем к непосредственному рассмотрению программного кода.


Предотвращение инъекционных атак посредством валидации вводимых данных

Общее правило разработки веб-приложения состоит в том, чтобы никогда не доверять "вслепую" входящей информации, предоставленной пользователем. Фильтрация и валидация входящих данных — это важнейшие методики противодействия инъекционным атакам, которые в обязательном порядке должны применяться ко всей подозрительной входной информации, полученной веб-приложением.

Среда Zend Framework предоставляет компонент Zend\InputFilter для фильтрации и валидации входных данных, а также обширный набор валидаторов для типовых вариантов использования. Компонент Zend\InputFilter лучше всего работает с компонентом Zend\Form, но, как будет показано позднее, ее также можно использовать и для валидации информации, вводимой в отдельную форму.

Для начала создайте пользовательскую форму с различными полями ввода посредством расширения компонента Zend\Form (см. листинг 1). Я использую эту форму для демонстрации нескольких ключевых характеристик компонента Zend\InputFilter.

Листинг 1. Объект формы
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Form\Fieldset;
use Application\Entity\Listing;
use Zend\Stdlib\Hydrator\ClassMethods;

class ListingForm extends Form
{
    public function __construct()
    {
        parent::__construct('listing_form');

        $this->setHydrator(new ClassMethods())
             ->setObject(new Listing());    
             
        $this->add(array(
            'name' => 'item_name',
            'options' => array(
                'label' => 'Item name',
            ),
            'attributes' => array(
                'size' => '30'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));

        $this->add(array(
            'name' => 'seller_name',
            'options' => array(
                'label' => 'Seller name',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'seller_email',
            'options' => array(
                'label' => 'Seller email address',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type' => 'Zend\Form\Element\Email',
        ));

        $this->add(array(
            'name' => 'item_min_price',
            'options' => array(
                'label' => 'Price (min)',
            ),
            'attributes' => array(
                'size' => '5'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'item_max_price',
            'options' => array(
                'label' => 'Price (max)',
            ),
            'attributes' => array(
                'size' => '5'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'validity',
            'options' => array(
                'label' => 'Available until',
                'render_delimiters' => false,
                'min_year'  => date('Y'),
                'max_year' => date('Y') + 5
        ),
            'type'  => 'Zend\Form\Element\DateSelect',
        ));  

        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'value' => 'Submit'
            ),
            'type'  => 'Zend\Form\Element\Submit',
        ));  
        
    }
}

В листинге 1 настраивается объект ListingForm, представляющий собой форму, с помощью которой продавцы могут добавлять товары в каталог на веб-сайте. На рис. 2 показана визуализированная версия этой формы. Даже если вы не знаете, как работает компонент Zend\Form, вам не составит никакого труда сопоставить объект в листинге 1 со стандартными элементами формы, показанными на рис. 2.

Рисунок 2. Форма ввода данных о товаре
Listing form

Когда пользователь представляет сведения о товаре, необходимо проверить введенные им данные, чтобы убедиться в их валидности и в отсутствии в них вредоносного кода. Именно этим занимаются компоненты Zend\Filter и Zend\Validate.

  • Компонент Zend\Filter сканирует введенную информацию и экранирует или удаляет недопустимые строки (HTML-элементы или маршруты файловой системы), а также преобразует ее в соответствии с требованиями форматирования (удаляя пробельные символы и переносы строк, изменяя регистр символов и т. д.).
  • Компонент Zend\Validate гарантирует валидность входящей информации и ее соответствие ожиданиям стороны, запрашивающей ввод данных. В частности, этот компонент проверяет формат адресов электронной почты, имена хостов и URI-идентификаторы; гарантирует заданную минимальную длину строк; проверяет точность номеров и почтовых индексов.

Отдельные фильтры и валидаторы можно связывать в цепочки с целью тестирования определенного входного значения; значение будет считаться валидным только после успешного прохождения всех валидаторов и фильтров в цепочке.

Компонент Zend\InputFilter позволяет группировать фильтры и валидаторы, что упрощает одновременную валидацию множества вводимых данных, например, нескольких данных, отправленных посредством формы. В листинге 2 на основе компонента Zend\InputFilter создается новый объект ListingFilter, который содержит все фильтры и валидаторы, необходимые для тестирования входящих данных, представленных посредством ListingForm.

Листинг 2. Валидаторы и фильтры информации, вводимой в форму
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;

class ListingFilter extends InputFilter 
{
    public function __construct() {
    
        $this->add(array(
            'name' => 'item_name',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'min' => 1,
                        'max' => 100,
                    ),
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'item_min_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'item_max_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
            ),            
        ));
        
        $this->add(array(
            'name' => 'seller_email',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'EmailAddress',
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'seller_name',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
           
        ));        
    }
}

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

В вашем распоряжении имеется обширный ассортимент из более чем 40 фильтров и 80 валидаторов; кроме того, вам никто не запрещает создавать собственные фильтры и валидаторы (вскоре я продемонстрирую соответствующий пример). В листинге 2 используются фильтр Zend\Filter\StripTags, который удаляет из введенной информации все HTML-элементы, и фильтр Zend\Filter\StringTrim, который удаляет из введенной информации необязательные пробелы. Выбор валидаторов осуществляется на основе характера входящей информации. Соответственно в листинге 2 используются валидатор Zend\Validator\EmailAddress как наиболее подходящий для вводимой информации 'seller_email', и валидатор Zend\I18n\Validator\Float, гарантирующий, что введенная информация 'item_max_price' является числовым значением.

После задания формы и фильтра необходимо связать их в рамках действия контроллера (controller action) (см. листинг 3).

Листинг 3. Обработчик формы
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\ListingForm;
use Application\Filter\ListingFilter;
use Application\Entity\Listing;

class ExampleController extends AbstractActionController
{
    public function validateAction()
    {
        // generate form and bind to object
        $form = new ListingForm();
        $listing = new Listing();
        $form->bind($listing);

        // set input filters
        $form->setInputFilter(new ListingFilter());
        $request = $this->getRequest();

        // validate and display form input
        if ($request->isPost()) {
            $form->setData($request->getPost());
            if ($form->isValid()) {
                print_r($listing);
                exit;
            }       
        }

        // render view 
        return new ViewModel(array('form' => $form));
    }
}

В листинге 3 производится инициализация объекта ListingForm, связывание этого объекта с пользовательским объектом Listing и присоединение к нему объекта ListingFilter. После отправки формы метод setData() компонента Zend\Form присваивает данные, отправленные (посредством POST) через форму, объекту формы, а метод isValid() проверяет входную информацию формы с использованием правил, заданных в объекте ListingFilter. После этого признанные валидными данные могут быть подвергнуты дальнейшей обработке: сохранены в базе данных, использованы в вычислениях, переданы в веб-сервис и т.д. В данном случае валидные и отфильтрованные данные просто визуализируются в представлении.

В листинге 4 содержится скрипт отображения, который визуализирует форму.

Листинг 4. Скрипт для отображения формы
<h2>Add Listing</h2>
<?php
// prepare form and set action
$form = $this->form;
$form->prepare();
$form->setAttribute('action', '/application/example/validate');
$form->setAttribute('method', 'post');
echo $this->form()->openTag($form);
?>

<div><?php echo $this->formRow($this->form->get('item_name')); ?></div>

<div><?php echo $this->formRow($this->form->get('item_min_price')); ?></div>

<div><?php echo $this->formRow($this->form->get('item_max_price')); ?></div>

<div><?php echo $this->formRow($this->form->get('seller_name')); ?></div>

<div><?php echo $this->formRow($this->form->get('seller_email')); ?></div>

<div><?php echo $this->formRow($this->form->get('validity')); ?></div>

<div><?php echo $this->formRow($this->form->get('submit')); ?></div>

<?php echo $this->form()->closeTag($form); ?>

На рис. 3 показан процесс валидации с использованием компонентов Zend\Form и Zend\InputFilter, которые совместно обнаруживают невалидную введенную информацию. На этом рисунке также показан результат успешного представления формы.

Рисунок 3. Валидация и визуализация формы
Form validation and submission

Сущность Listing в листинге 3 используется только для удобства. Сущности, введенные при посредстве объекта ListingForm, связаны с пользовательским объектом Listing, который представляет каждое введенное значение Listing как объект и предоставляет getter- и setter-методы для упрощения доступа к свойствам объекта. Для краткости код сущности Listing в статье не приводится, однако его можно найти в архиве кода, прилагаемом к статье.

Если вас не устраивают готовые валидаторы, можно создать собственный валидатор – расширив валидатор Zend\Validator\AbstractValidator (для сложных вариантов использования) или используя Zend\Validator\Callback для исполнения пользовательской функции валидации (для более простых вариантов использования). В качестве иллюстрации рассмотрим листинг 5, в котором в объект ListingFilter введен обратный вызов валидации для проверки условия, согласно которому максимальная цена всегда выше, чем минимальная цена.

Листинг 5. Обратный вызов пользовательской функции валидации
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;

class ListingFilter extends InputFilter 
{
    public function __construct() {
    
        // other inputs and validators
        
        $this->add(array(
            'name' => 'item_max_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
                array(
                    'name' => 'Callback',
                    'options' => array(
                        'messages' => array(
                            \Zend\Validator\Callback::INVALID_VALUE => 
                              'The maximum price is less than the minimum price',
                        ),
                        'callback' => function($value, $context=array()) {
                            $maxPrice = $value;
                            $minPrice = $context['item_min_price'];
                            $isValid = $maxPrice >= $minPrice;
                            return $isValid;
                        },
                    ),
                ),
            ),            
        ));

    }
}

В листинге 5 функция обратного вызова проверяет текущий контекст, извлекает значение 'item_min_price' и сравнивает его со значением переменной 'item_max_price', чтобы убедиться, что предыдущее значение меньше или равно последующему значению. На рис. 4 показан пример этой ситуации.

Рисунок 4. Валидация формы с зависимыми полями
Form validation with dependent fields

Конечно, компоненты Zend\InputFilter, Zend\Filter и Zend\Validate можно использовать и автономно. В качестве иллюстрации рассмотрим листинг 6, который содержит простую форму с полями для имени, возраста и номера кредитной карты.

Листинг 6. Форма
<html>
  <head></head>
  <body>
    <form method="post" action="register.php">
      <div>
        Name <br/>
        <input type="text" name="name" size="30" />
      </div>

      <div>
        Age  <br/>
        <input type="text" name="age" size="3" />
      </div>

      <div>
        Credit card number  <br/>
        <input type="text" name="cnum" size="25" />
      </div>

      <div>
        <input type="submit" name="submit" value="Submit">
      </div>
    
    </form>
  </body>

В листинге 7 настраивается валидация для формы, показанной в листинге 6.

Листинг 7. Обработчик формы с фильтрами и валидаторами
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\Validator;
use Zend\I18n;

$name = new Input('name');
$name->getFilterChain()
          ->attachByName('StripTags')
          ->attachByName('StringTrim');
$name->getValidatorChain()
          ->addValidator(new Validator\StringLength(array(
              'min' => '1',
              'max' => '100'
            )));

$age = new Input('age');
$age->getFilterChain()
               ->attachByName('StripTags')
               ->attachByName('StringTrim');
$age->getValidatorChain()
               ->addValidator(new Validator\GreaterThan(array(
                  'min' => '0'
               )))
               ->addValidator(new I18n\Validator\Float());

$cnum = new Input('cnum');
$cnum->getFilterChain()
             ->attachByName('StripTags')
             ->attachByName('StringTrim');
$cnum->getValidatorChain()
             ->addValidator(new Validator\CreditCard());

      
$inputFilter = new InputFilter();
$inputFilter->add($name)
            ->add($age)
            ->add($cnum)
            ->setData($_POST);
            
if ($inputFilter->isValid()) {
    print_r($inputFilter->getValues());
} else {
    echo "The form is not valid.<br/>";
    foreach ($inputFilter->getInvalidInput() as $invalidInput) {
        echo $invalidInput->getName() . ': ' . 
        implode(',',$invalidInput->getMessages()) . '<br/>';
    }
}

В листинге 7 сначала настраивается автозагрузчик Zend Framework, который загружает компоненты среды Zend Framework по мере необходимости. Обратите внимание: чтобы все это работало, библиотеки Zend Framework должны быть указаны в переменной include вашей PHP-среды.

После конфигурирования автозагрузчика следующий шаг состоит в создании объектов Zend\Filter\Input (по одному для каждой входной переменной) и в подключении к ним фильтров и валидаторов. В листинге 7 используются многие из тех же фильтров и валидаторов, которые присутствовали в листинге 2; примечательным дополнением является валидатор Zend\Validator\CreditCard, который упрощает проверку номера кредитной карты на внутреннюю непротиворечивость.

После описания входных объектов они группируются в объект Zend\InputFilter и заполняются данными из суперглобальной переменной $_POST. После этого вызывается метод isValid() объекта InputFilter для тестирования предоставленной входной информации, как было описано выше. Валидные значения можно извлечь с помощью метода getValues() объекта InputFilter; невалидные значения можно извлечь с помощью метода getInvalidInput().

На рис. 5 показан результат ввода в форму валидной и невалидной информации.

Рисунок 5. Валидация формы и визуализация результатов
Form validation and submission

Фильтрация запросов ботов с помощью CAPTCHA

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

Простейший способ остановить деятельность ботов – добавить к своей форме механизм под названием CAPTCHA. В состав среды Zend Framework входит компонент Zend\Captcha, предназначенный именно для этой цели. Компонент Zend\Captcha позволяет добавить к вашей форме механизм CAPTCHA на основе figlet-текста или изображения, а также поддерживает веб-сервис reCAPTCHA для дистанционной генерации CAPTCHA.

Обычно компонент Zend\Captcha наилучшим образом работает с компонентом Zend\Form(см. листинг 8).

Листинг 8. Объект формы с CAPTCHA на основе изображения
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Captcha\Image;
use Zend\Captcha\AdapterInterface;

class MessageForm extends Form
{

    protected $captcha;
    
    public function __construct()
    {
        parent::__construct('message_form');

        $this->captcha = new Image(array(
            'expiration' => '300',
            'wordlen' => '7',
            'font' => 'data/fonts/arial.ttf',
            'fontSize' => '20',
            'imgDir' => 'public/captcha',
            'imgUrl' => '/captcha'
        ));
        
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));

        $this->add(array(
            'name' => 'email',
            'options' => array(
                'label' => 'Email address',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Email',
        ));
        
        $this->add(array(
            'name' => 'message',
            'options' => array(
                'label' => 'Message',
            ),
            'attributes' => array(
                'rows' => '10',
                'cols' => '50'
            ),
            'type'  => 'Zend\Form\Element\Textarea',
        ));

        $this->add(array(
            'name' => 'captcha',
            'options' => array(
                'label' => 'Verification',
                'captcha' => $this->captcha,
            ),
            'type'  => 'Zend\Form\Element\Captcha',
        ));

        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'value' => 'Submit'
            ),
            'type'  => 'Zend\Form\Element\Submit',
        ));  
        
    }
}

В листинге 8 создается простая контактная форма с полями для ввода имени, адреса электронной почты и верификации с использованием CAPTCHA. Контент CAPTCHA генерируется компонентом Zend\Captcha\Image, который принимает несколько конфигурационных параметров (длина CAPTCHA-слова, шрифт CAPTCHA-слова и каталог, в котором хранится сгенерированное CAPTCHA-слово). Обратите внимание, что компонент Zend\Captcha\Image использует для генерации CAPTCHA-изображения PHP-расширение GD.

Компонент Zend\Captcha автоматически настраивает необходимые валидаторы, поэтому вам остается использовать его в рамках действия контроллера (листинг 9).

Листинг 9. Обработчик формы
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\MessageForm;
use Application\Filter\MessageFilter;

class ExampleController extends AbstractActionController
{
    
    public function captchaAction()
    {
        $form = new MessageForm();
        $form->setInputFilter(new MessageFilter());
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());
            if ($form->isValid()) {
                print_r($form->getData());
                exit;
            }       
        }
        return new ViewModel(array('form' => $form));
    }
}

На рис. 6 показана CAPTCHA на основе изображения в действии.

Рисунок 6. Форма с CAPTCHA на основе изображения
Form with image CAPTCHA

Скелетное приложение по умолчанию не загружает компонент ZendService\ReCaptcha автоматически, таким образом, если вы предпочитаете использовать сервис reCAPTCHA, то вам придется обновить конфигурацию Composer для приложения и добавить эту зависимость, как показано ниже.

{
    // other directives
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": "2.2.*",
        "zendframework/zendservice-recaptcha": "2.*"
    }
}

После этого снова запустите Composer для загрузки необходимых файлов и их установки в ваше приложение.

В листинге 10 показан измененный код формы, использующий сервис reCAPTCHA, а на рис. 7 показана сама измененная форма. Обратите внимание, что в процессе конфигурирования объекта Zend\Captcha\ReCaptcha вам необходимо указать свои открытые и закрытые ключи сервиса reCAPTCHA. На случай, если у вас нет этих ключей, в разделе Ресурсы приведена ссылка на веб-сайт сервиса reCAPTCHA, где вы сможете получить их бесплатно.

Листинг 10. Форма с интеграцией сервиса reCAPTCHA
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Captcha\ReCaptcha;
use Zend\Captcha\AdapterInterface;

class MessageForm extends Form
{

    protected $captcha;
    
    public function __construct()
    {
        parent::__construct('message_form');  

        $this->captcha = new Recaptcha(array(
            'privKey' => 'YOUR-PRIVATE-KEY',
            'pubKey' => 'YOUR-PUBLIC-KEY',
        ));

        // other elements
}
Рисунок 7. Форма с reCAPTCHA
Form with reCAPTCHA

На случай, если вы предпочитаете использовать компонент Zend\Captcha на автономной основе, в листинге 11 предлагается пример с использованием адаптера Figlet.

Листинг 11. Форма и обработчик формы с CAPTCHA на основе figlet-текста
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\Validator;
use Zend\Captcha;
            
$captcha = new Captcha\Figlet(array(
    'name' => 'captcha',
    'expiration' => '300',
    'wordlen' => '7',
));

if ($_POST) {

    $name = new Input('name');
    $name->getFilterChain()
         ->attachByName('StripTags')
         ->attachByName('StringTrim');
    $name->getValidatorChain()
         ->addValidator(new Validator\StringLength(array(
              'min' => '1',
              'max' => '100'
         )));

    $message = new Input('message');
    $message->getFilterChain()
            ->attachByName('StripTags')
            ->attachByName('StringTrim');
    $message->getValidatorChain()
            ->addValidator(new Validator\StringLength(array(
                'min' => '1'
            )));

    $verification = new Input('captcha');
    $verification->getValidatorChain()
                 ->addValidator($captcha);
                
    $inputFilter = new InputFilter();
    $inputFilter->add($name)
                ->add($message)
                ->add($verification)
                ->setData($_POST);
              
    if ($inputFilter->isValid()) {
        print_r($inputFilter->getValues());
    } else {
        echo "The form is not valid.<br/>";
        foreach ($inputFilter->getInvalidInput() as $invalidInput) {
            echo $invalidInput->getName() . ': ' . 
            implode(',',$invalidInput->getMessages()) . '<br/>';
        }
    }
    
} else {

    $id = $captcha->generate();
?>
<html>
  <head></head>
  <body>
    <form method="post" action="message.php">
      <div>
        Name <br/>
        <input type="text" name="name" size="30" />
      </div>

      <div>
        Message <br/>
        <textarea name="message" rows="10" cols="50"></textarea>
      </div>

      <div>
        Verification <br/>
        <pre><?php echo $captcha->getFiglet()->render(
          $captcha->getWord()); ?></pre>
        <input type="hidden" name="captcha[id]" value="<?php echo $id; ?>">
        <input type="text" name="captcha[input]" />
      </div>
      
      <div>
        <input type="submit" name="submit" value="Submit" />
      </div>
    
    </form>
  </body>
<?php
}
?>

В листинге 11 настраивается автозагрузчик Zend Framework и инициализируется новый объект Zend\Captcha\Figlet. В листинге используется метод generate() этого объекта для генерации новой CAPTCHA и идентификатора, а затем визуализируется форма с полями для имени, сообщения и верификации по CAPTCHA. Визуализация контента CAPTCHA также осуществляется внутри формы с помощью метода render() этого объекта. После представления формы компонент Zend\InputFilter используется для проверки введенного значения CAPTCHA на соответствие сгенерированной строке.

На рис. 8 показана форма с визуализированным контентом CAPTCHA.

Рисунок 8. Форма с CAPTCHA на основе figlet-текста
Form with figlet CAPTCHA

Блокирование спама с помощью сервиса Akismet

Еще один способ блокировки ввода спама в форму состоит в валидации вводимой информации с помощью хорошо известного веб-сервиса Akismet. Компонент ZendService\Akismet предоставляет сервисный объект, который позволяет с легкостью подключаться к сервису Akismet и с его помощью определять, являются ли конкретные входные данные спамом или полезной информацией.

Скелетное приложение по умолчанию не загружает компонент ZendService\Akismet автоматически, поэтому вам необходимо обновить конфигурацию Composer своего приложения и добавить эту зависимость, как показано ниже.

{
    // other directives
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": "2.2.*",
        "zendframework/zendservice-akismet": "2.*"
    }
}

В листинге 12 демонстрируется использование сервиса Akismet посредством добавления пользовательского обратного вызова для валидации показанной в листинге 8 формы MessageForm с помощью Akismet. Обратите внимание, что в процессе конфигурирования объекта ZendService\Akismet вам необходимо указать свой API-ключ Akismet. На случай, если у вас нет этого ключа, в разделе Ресурсы приведена ссылка на веб-сайт сервиса Akismet, где вы можете получить его бесплатно.

Листинг 12. Пользовательский валидационный обратный вызов с интеграцией сервиса Akismet
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;
use ZendService\Akismet\Akismet;

class MessageFilter extends InputFilter 
{
    public function __construct() {
    
         // other filters and validators

         $this->add(array(
            'name' => 'message',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Callback',
                    'options' => array(
                        'messages' => array(
                            \Zend\Validator\Callback::INVALID_VALUE => 
                              'The message is spam',
                        ),
                        'callback' => function($value, $context=array()) {
                            $akismet = new Akismet('YOUR-API-KEY', 
                              'http://example.localhost');
                            $data = array(
                                'user_ip' => $_SERVER['REMOTE_ADDR'],
                                'user_agent' => $_SERVER['HTTP_USER_AGENT'],
                                'comment_type' => '',
                                'comment_author' => $context['name'],
                                'comment_author_email' => $context['email'],
                                'comment_content' => $value
                            );
                            return ($akismet->isSpam($data)) ? false : true;
                        },
                    ),
                ),            
            )
        ));

    }
}

Пользовательский валидационный обратный вызов в листинге 12 инициализирует новый объект ZendService\Akismet, передавая ему API-ключ Akismet и URL-адрес приложения. Затем создается массив данных, соответствующий требованиям Akismet. Эти данные включают IP-адрес и браузер пользователя, отправляющего контент, а также имя пользователя и адрес его электронной почты. Этот массив передается в метод isSpam() объекта, который связывается с Akismet и получает в ответ логическое значение, указывающее, являются ли данные спамом по мнению Akismet.

На рис. 9 эта валидация показана в действии.

Рисунок 9. Валидация формы с помощью сервиса Akismet
Form with Akismet validation

Противодействие XSS-атакам посредством экранирования спецсимволов в выходной информации

Межсайтовый скриптинг (Cross-Site Scripting, XSS) — одно из наиболее распространенных направлений атак на веб-приложения. Однако неукоснительное применение экранирования спецсимволов (escaping) и кодирования позволяет сделать приложение полностью неуязвимым к этим атакам.

Предпочтительным инструментом в этой сфере является компонент Zend\Escaper, который предлагает пять методов для экранирования спецсимволов в выходной информации в зависимости от того, где эта информация будет присутствовать на визуализируемой странице. Существуют методы для экранирования спецсимволов/кодирования контента в содержимом HTML-страницы, атрибутах HTML-элемента, скриптовых элементах, стилевых элементах и URI.

В качестве иллюстрации рассмотрим листинг 13, в котором отображаются данные, полученные в результате отправки формы на веб-странице. Эти данные содержат типичный вектор XSS-атаки, упомянутый на веб-сайте сообщества OWASP; выходные данные отображаются с экранированием спецсимволов и без него, чтобы проиллюстрировать, как применение Zend\Escaper сводит атаку на нет.

Листинг 13. Выходные данные с экранированием спецсимволов
<?php
// предполагается, что данные поступили из запроса
$data =<<<'EOT'
';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";
alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//--
></SCRIPT>">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
EOT;
?>

<h2>Escaped Result</h2>
<?php echo $this->escapeHtml($data); ?>

<h2>Unescaped Result</h2>
<?php echo $data; ?>

На рис. 10 показана выходная информация. Как можно увидеть на этом рисунке, вывод данных в браузере без экранирования спецсимволов приводит к демонстрации предупреждения — классический пример XSS-атаки.

Рисунок 10. XSS-атака с выходной информацией без экранирования спецсимволов
XSS attack without output escaping

В дополнение к методу escapeHtml() компонент Zend\Escaper также предоставляет следующие методы.

  • Метод escapeHtmlAttr() для экранирования HTML-атрибута
  • Метод escapeJs() для экранирования Javascript
  • Метод escapeCss() для экранирования CSS
  • Метод escapeUrl() для экранирования спецсимволов в параметре URL

В приложении среды Zend Framework хелперы представлений (view helper) для всех предыдущих методов автоматически доступны в скриптах представления. В автономном приложении компонент Zend\Escaper необходимо инстанциировать перед использованием (см. листинг 14).

Листинг 14. Выходные данные с экранированием спецсимволов
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\Escaper\Escaper;

if ($_POST) {
  $escaper = new Escaper('utf-8');
  echo $escaper->escapeHtml($_POST['message']);
}

Журналирование событий и ошибок приложения

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

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

В листинге 15 показан простой пример компонента Zend\Log в действии.

Листинг 15. Регистрация событий приложения
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Log\Writer\Stream;
use Zend\Log\Logger;
use Zend\Log\Formatter\Xml;

class ExampleController extends AbstractActionController
{

    // other methods

    public function logAction()
    {
        $logger = new Logger();
        $writer = new Stream('data/log.xml');
        $formatter = new Xml();
        $writer->setFormatter($formatter);
        $logger->addWriter($writer);
        $logger->info('New product added by user');  
        return false;        
    }
}

Действие контроллера в листинге 15 начинается с инициализации нового экземпляра Zend\Log\Logger; это основная точка управления для взаимодействия со средой журналирования. Этот объект передается объекту Zend\Log\Writer\Stream, который используется для записи журнальных данных в PHP-потоки или в URL-адреса файловой системы; в этом случае он инициализируется с маршрутом к выходному журнальному файлу.

Существуют и другие средства записи (writer): объект Zend\Log\Writer\Db можно использовать для записи в любую PDO-совместимую базу данных, объект Zend\Log\Writer\Mail — для пересылки журнальных данных по электронной почте, объект Zend\Log\Writer\MongoDB — для записи в базу данных MongoDB, объект Zend\Log\Writer\FirePHP либо объект Zend\Log\Writer\ChromePHP — для записи в браузерную консоль FirePHP или ChromePHP.

В свою очередь, объект Zend\Log\Writer\Stream передает объект Zend\Log\Formatter\Xml, который берет на себя форматирование журнальных данных (например, в XML). Как и в случае со средствами записи, средства форматирования (formatter) существуют и для других целей, включая таблицы базы данных, FirePHP и ChromePHP; вы также можете создать собственные средства форматирования.

После конфигурирования цепочки "средства журналирования — средства записи — средства форматирования" вам достаточно лишь сгенерировать событие, подлежащее регистрации в журнале. Эта задача с легкостью решается с помощью метода log(), который принимает один из восьми заранее заданных уровней журнала и строку сообщения. Уровни журнала охватывают диапазон от 'debug' (отладочные сообщения) до 'emerg' (экстренные сообщения); для каждого из этих уровней доступен соответствующий ускоренный метод. В листинге 15 используется ускоренный метод info(), предназначенный для информационных сообщений. Для регистрируемых в журнале сообщений нет необходимости задавать метку времени, поскольку компонент Zend\Log добавляет ее автоматически.

На рис. 11 показан пример XML-записи в журнале.

Рисунок 11. XML-запись в журнале
XML log output

Кроме того, компонент Zend\Log можно использовать для регистрации ошибок приложения с целью последующего анализа и исправления. Для иллюстрации рассмотрим листинг 16, в котором компонент Zend\Log используется в качестве автономного компонента с определяемым пользователем обработчиком PHP-ошибок с целью автоматической регистрации ошибок приложения.

Листинг 16. Регистрация ошибок в журнале
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\Log\Writer\Stream;
use Zend\Log\Logger;
use Zend\Log\Formatter\Xml;

error_reporting(0);
set_error_handler('userErrorHandler');

function userErrorHandler($errno, $errstr, $errfile, $errline)  {
  $writer = new Stream('error.log');
  $logger = new Logger();
  $logger->addWriter($writer);
  $logger->err("$errstr: $errfile: $errline: " . json_encode($_REQUEST));  
}

// trigger errors
echo 5/0;
some_undef_func();
?>

В листинге 16 функция set_error_handler() используется для замены PHP-механизма обработки ошибок по умолчанию на определяемую пользователем функцию userErrorHandler(). Эта функция инициализирует новый регистрирующий компонент Zend\Log, присоединяет к нему компонент для записи Zend\Log\Writer\Stream, а затем регистрирует события типа "ошибка", указывая в журнальном файле имя исходного файла и номер строки с ошибкой. Строка сообщения, поступающая в регистрирующий компонент, для удобства отладки также содержит дамп объекта текущего запроса. Поскольку никакое средство форматирования не указано, компонент Zend\Log автоматически использует форматтер Zend\Log\Formatter\Simple, который генерирует по одной выходной строке на каждое событие в журнале.

На рис. 12 показан пример записи журнала, сгенерированный текстом в листинге 16:

Рисунок 12. Стандартная запись в журнале
Standard log output

Заключение

Как показывают показанные выше примеры, среда Zend Framework 2 включает в себя различные компоненты, которые облегчают защиту веб-приложения от инъекций и от XSS-атак, а также регистрацию событий и ошибок приложения в журнале. Это компоненты легко использовать в любом варианте применения среды Zend Framework — в виде MVC-реализации или в виде автономного PHP-приложения. В вашем распоряжении имеются все необходимые инструменты, чтобы приступить к защите своего приложения немедленно — так не теряйте ни минуты!


Загрузка

ОписаниеИмяРазмер
Пример Zend-кода для обеспечения безопасностиzf-security-code.zip12 КБ

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=976508
ArticleTitle=Улучшение защиты веб-приложений с помощью среды Zend Framework 2
publish-date=07032014