Мы рассмотрим использование двух предоставляемых Zend Framework 3
элементов безопасности форм: Captcha
и Csrf
[Zend\Form\Element\Csrf] (оба класса принадлежат
пространству имен Zend\Form\Element
). Добавление этих элементов на модель
формы (и их визуализация в шаблоне представления) сделает вашу форму устойчивой
к хакерским атакам.
CAPTCHA (расшифровывается как "Completely Automated Public Turing test to tell Computers and Humans Apart" - "полностью автоматизированный публичный тест Тьюринга для различения компьютеров и людей") - это тест типа «вызов - ответ», используемый веб-сайтами, чтобы определить, кем является пользователь: человеком или роботом.
Существует несколько типов CAPTCHA. Самый распространенный требует от пользователя ввести с клавиатуры буквы с искаженного изображения, показываемого на веб-странице (см. рисунок 11.1).
Типичный тест CAPTCHA использует следующий алгоритм:
Целью теста CAPTCHA является защита форм от заполнения и отправки автоматическим процессом (так называемым роботом). Обычно такие роботы рассылают спам-сообщения на форумах, взламывают пароли на формах входа на сайт или совершают какие-либо другие вредоносные действия.
Тест CAPTCHA позволяет надежно различать людей и роботов, потому что люди легко могут распознать и воспроизвести символы с искаженного изображения, в отличие от роботов, которые этого не умеют (на данном этапе эволюции алгоритмов компьютерного зрения).
В Zend Framework 3 доступно несколько типов CAPTCHA (все принадлежат
компоненту Zend\Captcha
):
Dumb. Это очень простой алгоритм CAPTCHA, который требует от пользователя ввести буквы слова в обратном порядке. Мы не будем рассматривать этот тип детально, так как он предоставляет слишком низкий уровень защиты.
Image. Алгоритм CAPTCHA, искажающий изображение добавлением шума в виде точек и кривых линий (рисунок 11.1, а).
Figlet. Редкий тип CAPTCHA, использующий программу FIGlet вместо алгоритма искажения изображения. FIGlet - это открытое программное обеспечение, генерирующее изображение CAPTCHA множества ASCII-букв (рисунок 11.1, б).
Компонент Zend\Captcha
предоставляет единообразный интерфейс для всех типов
CAPTCHA (интерфейс AdapterInterface
). Базовый класс AbstractAdapter
реализует
этот интерфейс, и все остальные алгоритмы CAPTCHA наследуются от абстрактного
класса адаптера 45. Диаграмма наследования классов показана ниже на рисунке 11.2.
45) Адаптер - это шаблон проектирования, который преобразовывает интерфейс класса в
совместимый интерфейс, что позволяет двум (или нескольким) несовместимым интерфейсам
работать вместе. Как правило, у алгоритмов CAPTCHA различаются public-методы, но, так
как они все реализуют интерфейс AbstractAdapter
, их все можно использовать
одним и тем же способом (вызывая методы, предоставляемые базовым интерфейсом)
Как видите из рисунка 11.2, существует еще один базовый класс для всех типов CAPTCHA,
который использует некое секретное слово из символов: класс AbstractWord
. Этот
базовый класс предоставляет методы для генерации случайной последовательности символов
и для настройки опций генерации слов.
ZF3 предоставляет специальный класс элемента формы и класс помощника вида для использования полей CAPTCHA на формах.
Чтобы добавить поле CAPTCHA к модели формы, используйте класс Captcha
, который принадлежит компоненту Zend\Form
и "живет" в пространстве имен Zend\Form\Element
.
Класс элемента Captcha
может быть использован с любым алгоритмом CAPTCHA (перечислены
в предыдущем разделе) из компонента Zend\Captcha
. Для этой цели, класс элемента имеет
метод setCaptcha()
, который принимает либо экземпляр класса, реализующего интерфейс
Zend\Captcha\AdapterInterface
, либо массив, содержащий настройку CAPTCHA 46.
Методом setCaptcha()
можно добавить к элементу желаемый тип CAPTCHA.
46) Во втором случае (описание в виде массива) алгоритм CAPTCHA будет
автоматически инстанцирован и инициализирован классом фабрики Zend\Captcha\Factory
.
Элемент Captcha
добавляется к модели формы обычным способом, через метод add()
,
предоставляемый базовым классом Zend\Form\Form
. Как обычно, вы можете передать методу
либо экземпляр класса Zend\Form\Element\Captcha
, либо массив опций конфигурации для
конкретного алгоритма CAPTCHA (в этом случае элемент и его алгоритм CAPTCHA будут автоматически
инстанцированы и настроены классом фабрики).
Пример кода ниже показывает, как использовать второй способ (передача конфигурации в виде массива).
Мы предпочитаем его, так как он требует меньше кода. Предполагается, что вы вызываете этот код
внутри protected-метода addElements()
модели формы:
<?php
// Добавляем поле CAPTCHA к модели формы
$this->add([
'type' => 'captcha',
'name' => 'captcha',
'options' => [
'label' => 'Human check',
'captcha' => [
'class' => '<captcha_class_name>', //
// Здесь идут опции для конкретного класса ...
],
],
]);
В этом фрагменте мы вызываем метод add()
, предоставляемый базовым классом Form
,
и передаем ему описание элемента, который хотим добавить, в виде массива (строка 3):
type
(строка 4), как и обычно, может быть либо полностью определенным именем класса
элемента (Zend\Form\Element\Captcha
), либо его псевдонимом ("captcha").name
(строка 5) - это значение атрибута "name" поля HTML-формы.options
содержии опции для присоединенного алгоритма CAPTCHA.class
(строка 9) может содержать либо полное имя класса CAPTCHA (например, Zend\Captcha\Image
),
либо его псевдоним (например, "Image"). Другие опции, специфичные для определенного адаптера, также
могут быть добавлены к этому ключу. Немного позже мы покажем, как это сделать.Для генерации HTML-разметки для элемента, можно использовать класс помощника вида FormCaptcha
(принадлежащий пространству имен Zend\Form\View\Helper
). Однако, как вы могли узнать из предыдущей
главы, как правило, вместо этого используется общий помощник вида FormElement
, как показано в коде ниже:
<?= $this->formElement($form->get('captcha')); ?>
Предполагается, что вы вызываете помощник вида внутри шаблона представления.
Далее мы приведем два примера, показывающие, как использовать два разных типа CAPTCHA,
предоставляемых ZF3: Image
и Figlet
. Мы покажем, как добавить поле CAPTCHA к форме
обратной связи, которую мы использовали в примерах предыдущих глав.
Для CAPTCHA типа Image требуется установленное PHP-расширение GD с поддержкой PNG и шрифтами FT.
Чтобы добавить CAPTCHA Image
к модели формы, вызовите метод формы
add()
как показано ниже:
<?php
namespace Application\Form;
// ...
class ContactForm extends Form
{
// ...
protected function addElements()
{
// ...
// Add the CAPTCHA field
$this->add([
'type' => 'captcha',
'name' => 'captcha',
'attributes' => [
],
'options' => [
'label' => 'Human check',
'captcha' => [
'class' => 'Image',
'imgDir' => 'public/img/captcha',
'suffix' => '.png',
'imgUrl' => '/img/captcha/',
'imgAlt' => 'CAPTCHA Image',
'font' => './data/font/thorne_shaded.ttf',
'fsize' => 24,
'width' => 350,
'height' => 100,
'expiration' => 600,
'dotNoiseLevel' => 40,
'lineNoiseLevel' => 3
],
],
]);
}
}
В этом фрагменте ключ массива конфигурации captcha
(строка 20) содержит следующие
параметры для настройки алгоритма CAPTCHA Image
, присоединенного к элементу формы:
параметр class
(строка 21) должен быть либо полностью определенным именем класса
адаптера CAPTCHA (\Zend\Captcha\Image
), либо его псевдоним (Image
).
параметр imgDir
parameter (line 22) должен быть путем к каталогу, куда нужно
сохранять сгенерированные искаженные изображения (в этом примере мы будем сохранять)
изображения в каталог APP_DIR/public/img/captcha).
параметр suffix
(строка 23) определяет расширение для сгенерированного файла
изображения (в этом примере - ".png").
параметр imgUrl
(строка 24) определяет базовую частью URL для открытия сгенерированных
изображений CAPTCHA в веб-браузере. В этом примере посетители сайта смогут обратиться к
изображениям CAPTCHA используя URL типа "http://localhost/img/captcha/<ID>", где ID -
уникальный идентификатор определенного изображения.
опциональный параметр imgAlt
(строка 25) - это альтернативный текст, который показывается, если изображение
CAPTCHA не может быть загружено браузером (атрибут "alt" тега <img>
).
параметр font
(строка 26) - это путь к файлу шрифта. Вы можете скачать бесплатный шрифт TTF,
например, отсюда. В этом примере мы используем шрифт Thorne Shaded,
который мы скачали и поместили в файл APP_DIR/data/font/thorne_shaded.ttf.
параметр fsize
(строка 27) - положительное число типа integer, определяющее размер шрифта.
параметры width
(строка 28) и height
(строка 29) определяют соответственно ширину и высоту (в пикселях)
сгенерированного изображения.
параметр expiration
(строка 30) определяет срок действия изображений CAPTCHA (в секундах).
Как только срок действия изображения истекает, оно удаляется с диска.
параметры dotNoiseLevel
(строка 31) и lineNoiseLevel
(строка 32) определяют
опции генерации изображения (уровни точечного и линейного шумов соответственно).
Для визуализации поля CAPTCHA добавьте следующие строки в файл шаблона представления contact-us.phtml :
<div class="form-group">
<?= $this->formLabel($form->get('captcha')); ?>
<?= $this->formElement($form->get('captcha')); ?>
<?= $this->formElementErrors($form->get('captcha')); ?>
<p class="help-block">Введите буквы, которые вы видите на изображении выше.<p>
</div>
Теперь создадим каталог APP_DIR/public/img/captcha, который будет хранить сгенерированные
изображение CAPTCHA. Отрегулируем права доступа к каталогу, чтобы он был доступным для записи
для веб-сервера Apache. В Linux Ubuntu это, как правило, совершается следующими командами оболочки
(замените плейсхолдер APP_DIR
именем каталога вашего веб-приложения):
mkdir APP_DIR/public/img/captcha
chown -R www-data:www-data APP_DIR
chmod -R 775 APP_DIR
Команда mkdir
создает каталог, а команды chown
и chmod
соответственно назначают
пользователя Apache владельцем каталога и позволяют веб-серверу записывать данные в каталог.
Если вы теперь откроете страницу "http://localhost/contactus" в своем браузере, будет сгенерировано изображение CAPTCHA, основанное на случайной последовательности букв и цифр, сохраненных в сессии. Вы должны будете увидеть что-то вроде рисунка 11.3.
Когда вы заполните поля формы и нажмете кнопку Submit, введенные в поле Human check
буквы будут переданы серверу как часть HTTP-запроса. Затем, при валидации формы, класс
Zend\Form\Element\Captcha
сравнит отправленные буквы с теми, что хранятся в PHP-сессии.
Если буквы идентичны, форма считается действительной; иначе форма не проходит валидацию.
Как только визуализатор PHP обработает шаблон представления, он сгенерирует HTML-разметку для элемента CAPTCHA как показано ниже:
<div class="form-group">
<label for="captcha">Human check</label>
<img width="350" height="100" alt="CAPTCHA Image"
src="/img/captcha/df344b37500dcbb0c4d32f7351a65574.png">
<input name="captcha[id]" type="hidden"
value="df344b37500dcbb0c4d32f7351a65574">
<input name="captcha[input]" type="text">
<p class="help-block">Введите буквы, которые вы видите на изображении выше.</p>
</div>
Для использования на форме элемента CAPTCHA типа FIGlet, замените определение элемента формы из предыдущего примера следующим кодом:
<?php
// Добавляем поле CAPTCHA
$this->add([
'type' => 'captcha',
'name' => 'captcha',
'attributes' => [
],
'options' => [
'label' => 'Human check',
'captcha' => [
'class' => 'Figlet',
'wordLen' => 6,
'expiration' => 600,
],
],
]);
В этом фрагменте ключ массива конфигурации captcha
(строка 10) содержит следующие
параметры для настройки алгоритма CAPTCHA Figlet
, присоединенного к элементу формы:
параметр class
(строка 11) должен быть либо полностью определенным именем класса
адаптера CAPTCHA (\Zend\Captcha\Figlet
), либо его псевдоним (Figlet
).
параметр wordLen
(строка 12) длину генерируемого секретного слова.
параметр expiration
(строка 13) определяет срок действия CAPTCHA (в секундах).
Теперь откройте страницу "http://localhost/contactus" в своем браузере. Вы должны будете увидеть страницу как на рисунке 11.4 ниже.
Как только визуализатор PHP обработает шаблон представления, он сгенерирует HTML-разметку для элемента CAPTCHA как показано ниже:
<div class="form-group">
<label for="captcha">Проверка пользователя</label>
<pre>
__ _ __ __ _ _ ___ _ _ __ __
| || | || \ \\/ // | \ / || / _ \\ | || | || \ \\/ //
| '--' || \ ` // | \/ || | / \ || | || | || \ ` //
| .--. || | || | . . || | \_/ || | \\_/ || | ||
|_|| |_|| |_|| |_|\/|_|| \___// \____// |_||
`-` `-` `-`' `-` `-` `---` `---` `-`'
</pre>
<input name="captcha[id]" type="hidden"
value="b68b010eccc22e78969764461be62714">
<input name="captcha[input]" type="text">
<p class="help-block">Введите буквы, которые вы видите на изображении выше.</p>
</div>
Подделка межсайтовых запросов (Сross Site Request Forgery - CSRF) - тип хакерской атаки, заставляющий браузер пользователя передать HTTP-запрос произвольному сайту. Через атаку CSRF вредоносный скрипт может послать неавторизованные команды от доверенного пользователя. Такая атака, как правило, совершается на страницы, содержащие формы для отправки каких-либо уязвимых данным (например, формы для перевода денег, корзины и т.д.)
Чтобы лучше понять, как работает эта атака, посмотрите на рисунок 11.5.
Рисунок 11.5 демонстрирует пример CSRF-атаки на веб-сайт платежного шлюза:
Вы заходите на свой аккаунт на сайте платежного шлюза https://payment.com. Заметьте, что в данном случае используется SSL-защищенное соединение (HTTPS), но оно не защищает от такого рода атак.
Как правило, вы устанавливаете флажок формы входа на сайт "Remember Me" (запомнить меня), чтобы избежать слишком частого ввода своих имени пользователя и пароля. Как только вы заходите на свой аккаунт, ваш браузер сохраняет информацию о сессии в переменную cookie на вашем компьютере.
На сайте платежного шлюза вы используете платежную форму https://payment.com/moneytransfer.php для покупки товаров. Обратите внимание, что эта форма позже будет использована в качестве уязвимости, что позволит осуществить CSRF-атаку.
Затем вы используете этот же браузер, чтобы посетить какой-либо сайт. Предположим,
это будет веб-сайт с классными картинками http://coolpictures.com. К сожалению,
этот сайт заражен вредоносным скриптом, замаскированным под HTML-тег <img src="image.php">
.
Как только вы откроете HTML-страницу в своем браузере, она загрузит все свои изображения,
тем самым выполним вредоносный скрипт image.php.
Вредоносный скрипт проверяет переменную cookie и, если таковая есть, он совершает захват сессии (session riding), что позволяет ему действовать от имени залогиненного пользователя. Теперь скрипт может отправлять данные платежной формы на сайт платежного шлюза.
Описанная выше CSRF-атака возможна в том случае, если веб-форма на сайте платежного шлюза не проверяет источник HTTP-запроса. Те, кто занимается поддержкой подобных сайтов, должны уделять больше внимания созданию максимально безопасных форм.
Чтобы предотвратить CSRF-атаку на форму, необходимо требовать специальный токен формы. Эта процедура описана ниже:
To prevent CSRF attacks to a form, one has to require a special token with the form, as follows:
Для определенной формы генерируется случайная последовательность байт (токен) и сохраняется на стороне сервера в данных PHP-сессии.
К форме добавляется скрытое поле, его значение задается токеном.
Когда пользователь отправляет форму, скрытое значение, переданное в форме, сравнивается с токеном, сохраненном на стороне сервера. Если они совпадают, данные формы считаются безопасными.
Если вредоносный пользователь попробует атаковать сайт через отправку формы, он не сможет поместить нужный токен в форму перед отправкой, так как токен не хранится в файлах cookie.
В Zend Framework 3, чтобы добавить CSRF-защиту к модели формы,
используется класс элемента формы Zend\Form\Element\Csrf
.
У элемента
Csrf
нет визуального представления (вы не увидите его на экране).
Чтобы добавить элемент CSRF к модели формы, добавьте следующие строки в метод addElements()
:
// Добавляем поле CSRF
$this->add([
'type' => 'csrf',
'name' => 'csrf',
'options' => [
'csrf_options' => [
'timeout' => 600
]
],
]);
В этом фрагменте мы используем метод add()
класса Form
(строка 2), которому передается
описание элемента CSRF в виде массива. Элемент будет автоматически инстанцирован и инициализирован
фабрикой.
В строке 3 мы указываем имя класса для элемента CSRF. Это может быть либо полностью определенное
имя класса (Zend\Form\Element\Csrf
), либо псевдоним ("csrf").
В строке 4 мы задаем атрибут "name" для элемента. В этом примере мы используем имя "csrf", но вы можете использовать любое другое имя по вашему выбору.
В строке 6, внутри массива csrf_options
, мы указываем опции для класса Zend\Form\Element\Csrf
.
Мы задаем значение опции timeout
, равное 600 (строка 7), что значит, срок действия CSRF-проверки
закончится спустя 600 секунд (10 минут) после создания формы.
Для визуализации поля CSRF добавьте следующую строку в файл шаблона представления .phtml:
<?= $this->formElement($form->get('csrf')); ?>
Как только визуализатор PHP обработает шаблон представления, он сгенерирует HTML-разметку для поля CSRF как показано ниже:
<input type="hidden" name="csrf" value="1bc42bd0da4800fb55d16e81136fe177">
Как видите из кода разметки выше, форма теперь содержит скрытое поле со случайно сгенерированным токеном. Так как атакующий скрипт не знает этот токен, он не сможет отправить его корректное значение, тем самым CSRF-атака будет предотвращена.
Что происходит при неудачной валидации элемента CSRF?
Если при валидации формы CSRF-проверка завершается неудачей, форма считается недействительной, и она будет снова показана пользователю для исправления ошибок, однако он не увидит сообщения об ошибке для элемента CSRF (мы не хотим, чтобы хакеры знали наверняка, что не так с формой).