Книга «Безопасность в PHP» (часть 1)
Книга «Безопасность в PHP» (часть 2)
Книга «Безопасность в PHP» (часть 3)


Связь через интернет между участниками процесса сопряжена с рисками. Когда вы отправляете платёжное поручение в магазин, используя онлайн-сервис, то совершенно точно не хотите, чтобы злоумышленник мог его перехватить, считать, изменить или заново повторить HTTP-запрос к онлайн-приложению. Только представьте себе последствия того, что атакующий считает куку вашей сессии или изменит получателя платежа, товар, платёжный адрес. Или внедрит в разметку, отправляемую магазином в ответ на запрос пользователя, свой HTML или JavaScript.


Защита важных и личных данных — это серьёзный бизнес. У пользователей браузеров и приложений крайне высокие ожидания относительно безопасности. Особое значение имеют целостность транзакций по банковским картам, приватности и идентификационной информации. Решение этих задач, когда речь идёт о защите передачи данных между двумя участниками процесса, требует обеспечения безопасности на транспортном уровне, куда обычно входят HTTPS, TLS и SSL.


Основные задачи мер защиты:


  • Безопасное шифрование пересылаемых данных.
  • Гарантированная идентификация одной или обеих сторон информационного обмена.
  • Защита от искажения данных.
  • Защита от атак с повторным воспроизведением.

Важнейший момент: для успешной защиты на транспортном уровне необходимо решить все четыре задачи. Если хоть в чём-то мы не преуспеем, то нас ждут серьёзные проблемы.


Многие ошибочно полагают, что шифрование — это ключевая задача, а все остальные необязательны. Это совершенно неверно. При шифровании пересылаемых данных получатель должен иметь возможность их расшифровать. Это возможно тогда, когда клиент и сервер договорились о ключе шифрования (помимо прочего) в ходе фазы переговоров при попытке клиента установить безопасное соединение. Однако злоумышленник способен внедриться между клиентом и сервером с помощью простых методов. Он может заставить клиентскую машину считать его сервером, это называется атакой «человек посередине» (MitM, Man-in-the-Middle). И тогда переговоры о ключе шифрования будут вестись не с настоящим сервером, а с фальшивым. Это позволит злоумышленнику расшифровывать все пересылаемые клиентом данные. Очевидно, что для защиты от этого сценария нам нужно соблюсти второе требование: возможность проверки идентификации сервера, с которым общается клиент. Без этой проверки мы не отличим целевой сервер от подставного.


Так что для безопасной связи необходимо соблюдать все четыре условия. Каждое из них идеально дополняет остальные три, и только все вместе они обеспечивают надёжную и устойчивую безопасность на транспортном уровне (Transport Layer Security, TLS).


Помимо технических аспектов работы TLS, у качественного обеспечения безопасности есть и другая сторона. Например, если мы позволяем пользователю вводить в форму авторизации приложения данные по HTTP, то должны для себя принять возможность MitM-атаки, в ходе которой будут перехвачены и в дальнейшем использованы авторизационные данные. Или если мы позволяем страницам, которые грузятся по HTTPS, подгружать не HTTPS-ресурсы, то должны для себя принять, что у MitM-злоумышленника есть транспорт, с помощью которого он может провести атаки межсайтового скриптинга и превратить пользовательский браузер в заранее запрограммированное оружие, способное прозрачно действовать через браузерное HTTPS-соединение.


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


  • Шифрование: используются ли сильные стандарты шифрования и наборы шифров?
  • Идентификация: проверяется ли корректность и полнота идентификации сервера?
  • Искажение данных: полностью ли защищаются пользовательские данные в ходе сессии?
  • Атаки с повторным воспроизведением: имеются ли методы защиты от злоумышленника, который записывает запросы, чтобы затем снова отправить их на сервер и многократно воспроизвести известные действия или эффекты?

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


Ещё один важный момент — какие данные должны защищаться. Очевидно, что это реквизиты банковских карт, информация, позволяющая установить личность, и пароли. А что насчёт ID пользовательской сессии? Если мы защищаем пароли, но не ID, то злоумышленник всё ещё может украсть передаваемые куки и выполнить атаку с перехватом сеанса (Session Hijacking attack), олицетворив собой пользователя на его же собственном компьютере. Одной лишь защиты форм авторизации НИКОГДА не достаточно для сохранения авторизационных данных. Лучшая защита достигается, если пользовательская сессия выполняется только в рамках HTTPS с момента ввода данных в форму до завершения сессии.


Теперь вам нужно понять, почему возникло слово «недостаточно». Проблема реализации SSL/TLS заключается не только в их неиспользовании, но и в использовании в недостаточной для максимальной безопасности мере.


Мы рассмотрим недостаточность безопасности на транспортном уровне с трёх точек зрения:


  • Между серверным приложением и сторонним сервером.
  • Между клиентом и серверным приложением.
  • Между клиентом и серверным приложением с использованием кастомных политик защиты.

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


Ко второму пункту относится взаимодействие пользователей с веб-приложением посредством браузеров или других клиентских приложений. В данном случае мы раскрываем безопасный URL, и нам нужно удостовериться, что меры безопасности реализованы корректно, чтобы не было риска их обхода.


К третьему пункту относятся всякие причудливые решения (curious oddity). Поскольку SSL/TLS имеют репутацию некорректно реализуемых программистами стандартов, существует масса способов обеспечить безопасное подключение без их участия. В качестве примера можно привести использование протоколом OAuth подписанных запросов, которые не требуют SSL/TLS, но предлагают ряд обеспечиваемых теми защитных мер (в частности, опущено шифрование данных запроса). Так что это не идеальное решение, но всё же оно лучше, чем неправильно сконфигурированная SSL/TLS-библиотека.


Прежде чем перейти к подробностям, давайте сначала рассмотрим TLS в целом и получим базовые знания, а потом углубимся во внутренности PHP.


Определения и базовые уязвимости


TLS — общее название, описывающее меры по созданию безопасного соединения между двумя участниками с помощью шифрования, проверки идентификации и т. д. Большинство из вас уже знакомы с тремя аббревиатурами: HTTPS, SSL, TLS. Давайте кратко пробежимся по ним и по их взаимосвязям.


SSL/TLS из PHP (сервер-сервер)


Как бы я ни любил PHP как язык программирования, но даже самый поверхностный обзор популярных open source библиотек ясно даёт понять: в них на каждом шагу встречаются уязвимости, связанные с безопасностью на транспортном уровне. И PHP-сообщество терпит эти уязвимости без какой-либо уважительной причины только потому, что проще подвергать пользователей угрозам, чем решать проблемы. Ситуацию усугубляет то, что сам PHP страдает от очень плохой реализации SSL/TLS в PHP-потоках, используемых всюду — от HTTP-клиентов на базе сокетов до file_get_contents() и прочих функций файловой системы. Добавьте к этому тот факт, что авторы PHP-библиотек даже не пытаются обсуждать возможные последствия сбоев SSL/TLS для безопасности.


Если вы не будете предпринимать ничего из описанного в этой главе, то хотя бы выполняйте все HTTPS-запросы с помощью расширения cURL для PHP. Его конфигурация по умолчанию обеспечивает безопасность, к тому же расширение опирается на экспертную оценку большого количества пользователей вне сферы PHP. Так что сделайте этот простой шаг к улучшению безопасности, и вы не пожалеете. Идеальным же решением будет, если авторы PHP наконец очнутся и внедрят во встроенную поддержку SSL/TLS принцип Secure By Default (безопасность по умолчанию).


Моё введение в SSL/TLS в PHP получилось очень грубым. TLS-уязвимости гораздо проще, чем большинство проблем в сфере безопасности, и все мы знаем, какое значение это имеет для браузеров. Но наше серверное приложение — не менее важное звено цепочки обеспечения безопасности пользовательских данных. Так что давайте подробнее рассмотрим SSL/TLS в PHP, в свою очередь, глядя на PHP-потоки и превосходное расширение cURL.


PHP-потоки


Для тех, кто не знаком с потоками: они нужны для обобщения (generalize) файлов, сети и прочих операций, совместно использующих общую функциональность. Чтобы поток знал, как работать с конкретным протоколом, применяются «обёртки», позволяющие потоку представлять файл, HTTP-запрос, PHAR-архив, URI данных (RFC 2397) и т. д. Чтобы инициировать поток, достаточно вызвать функцию вспомогательного файла (supporting file function) с соответствующим URL, которые обозначают обёртку и целевой ресурс.


file_get_contents('file:///tmp/file.ext');

По умолчанию потоки используют файловую обёртку (File Wrapper), так что обычно вам не нужен URL, достаточно даже относительного пути к файлам. Это очевидно, поскольку большинство функций файловой системы вроде file(), include(), require_once и file_get_contents() принимают поточные ссылки (stream references). Так что перепишем предыдущий пример:


file_get_contents('/tmp/file.ext');

Учитывая обсуждаемую тему, можно сделать и так:


file_get_contents('http://www.example.com');

Поскольку функции файловой системы — например file_get_contents() — поддерживают обёрнутые в HTTP потоки (HTTP wrapped streams), то они формируют в PHP HTTP-клиент, к которому очень просто получить доступ. Их можно использовать, если вы не чувствуете необходимости применять выделенные библиотеки для создания HTTP-клиентов вроде Guzzle, Buzz или классов из фреймворка Zend \Zend\Http\Client. Чтобы ваш простенький клиент заработал, нужно включить опцию allow_url_fopen в файле php.ini. По умолчанию она включена.


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


Не забывайте о том, что файлы будут подгружаться из localhost, а значит, смогут обходить контроль доступа на основе ограничений, прописанных на локальном сервере. И если опция allow_url_fopen включена по умолчанию, то для наибольшей безопасности её придётся отключить.


Вернёмся к использованию PHP-потоков в качестве простого HTTP-клиента (а теперь вы знаете, что это не рекомендуется). Всё становится интереснее, если вы попробуете сделать так:


$url = 'https://api.twitter.com/1/statuses/public_timeline.json';
$result = file_get_contents($url);

Это простой неаутентифицированный запрос через HTTPS к (бывшему) Twitter API 1.0. Здесь есть серьёзная утечка. Для запросов, выполняемых с помощью HTTPS- (https://) и FTPS- (ftps://) обёрток, PHP использует SSL Context. Здесь есть много настроек для SSL/TLS, а также их значений по умолчанию, которые абсолютно небезопасны. Перепишем пример так, чтобы проиллюстрировать, как можно вставить исходный набор настроек SSL Context в качестве параметра в file_get_contents():


$url = 'https://api.twitter.com/1/statuses/public_timeline.json';
$contextOptions = array(
    'ssl' => array()
);
$sslContext = stream_context_create($contextOptions);
$result = file_get_contents($url, NULL, $sslContext);

Как говорилось выше, если вы неправильно сконфигурируете SSL/TLS, то приложение будет беззащитно перед атаками «человек посередине». PHP-потоки по умолчанию совершенно небезопасны при работе через SSL/TLS. Так что давайте исправим наш пример, чтобы он стал полностью безопасен.


$url = 'https://api.twitter.com/1/statuses/public_timeline.json';
$contextOptions = array(
    'ssl' => array(
        'verify_peer'   => true,
        'cafile'        => '/etc/ssl/certs/ca-certificates.crt',
        'verify_depth'  => 5,
        'CN_match'      => 'api.twitter.com',
        'disable_compression' => true,
        'SNI_enabled'         => true,
        'ciphers'             => 'ALL!EXPORT!EXPORT40!EXPORT56!aNULL!LOW!RC4'
    )
);
$sslContext = stream_context_create($contextOptions);
$result = file_get_contents($url, NULL, $sslContext);

Теперь всё в порядке! Если сравнить с предыдущей версией, то вы заметите, что мы настроили четыре опции, которые изначально были не настроены или отключены PHP. Давайте разберёмся, что они делают.


  • verify_peer

Проверка пира (Peer Verification) — проверка достоверности SSL-сертификата, предоставленного хостом, на который мы отправили HTTPS-запрос. Правильный сертификат подписан закрытым ключом надёжного удостоверяющего центра (Certificate Authority, CA). Проверку можно выполнить с помощью открытого ключа CA, который включается в файловый набор в виде опции cafile для используемого нами SSL Context. Кроме того, сертификат не должен быть просрочен.


  • cafile

Настройка cafile должна указывать на валидный файл, содержащий открытый ключ надёжного CA. В PHP это не делается автоматически, поэтому держите ключи в связанном файле специального формата (обычно PEM или CRT). Если вы не можете найти копию, то скачайте и спарсите из Mozilla VCS. Без этого файла невозможно проверить пир, и запрос не будет выполнен.


  • verify_depth

Эта настройка задаёт максимально допустимое количество промежуточных издателей сертификатов, т. е. количество CA-сертификатов, которые могут использоваться при проверке исходного клиентского сертификата.


  • CN_match

Три предыдущие опции относятся к проверке предоставляемого сервером сертификата. Однако они не помогают нам понять, валиден ли он запрашиваемому нами доменному имени или IP, т. е. части URL, относящейся к хосту. Чтобы выяснить, привязан ли сертификат к текущему домену/IP, выполним проверку хоста (Host Verification). В PHP для этого нужно задать CN_match значение хоста (в SSL Context), включая поддомен, если есть. Пока задана эта опция, PHP будет выполнять внутреннюю проверку. Если же этого не делать, то во время атаки «человек посередине» злоумышленник может предоставить валидный сертификат, подписанный надёжным CA. Но сертификат будет валидным для домена, находящегося под контролем атакующего, а не для того домена, к которому вы хотели подключиться. Настройка опции CN_match поможет определить несовпадение сертификатов и приведёт к невыполнению HTTPS-запроса.


Поскольку используемый злоумышленником валидный сертификат будет содержать идентификационную информацию атакующего (это условие его получения!), то имейте в виду, что подкованному злоумышленнику доступно любое количество валидных, подписанных CA сертификатов в комплекте с соответствующими закрытыми ключами. Они могли быть украдены у других компаний либо проскользнуть через проверку надёжных CA. Так это произошло в 2011 году, когда DigiNotor выпустил для google.com сертификат для неизвестной стороны. Она использовала его для атак «человек посередине», преимущественно против иранских пользователей.


  • disable_compression

Эта опция появилась в PHP 5.4.13. Она нужна для защиты от атак CRIME и других атак с дополнением блоков наподобие BEAST. На момент написания этой части книги опция была доступна уже 10 месяцев. Потребовалось немало терпения, чтобы обнаружить практически единственный пример её использования в open source PHP.


  • SNI_enabled

Включает поддержку указания имени сервера (Server Name Indication), когда любой одиночный IP может быть настроен для работы нескольких SSL-сертификатов, чтобы не ограничиваться одним сертификатом для всех сайтов или не HTTP-сервисов, хостящихся на этом IP.


  • ciphers

Эта настройка помогает отображать, какие шифры следует или не следует выбирать при установке SSL/TLS-соединений. Список по умолчанию предоставляется расширением openssl. Он содержит небезопасные шифры, которые стоит отключить, если только вы не вынуждены их применять. Нижеприведённый список, использующий принятый в openssl синтаксис, был реализован cURL в январе 2014-го. Предложенный Mozilla альтернативный список может быть лучше, поскольку упор в нём сделан на полную безопасность пересылки (Perfect Forward Secrecy), это лучший практический подход. Список Mozilla длиннее:


ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK

Ограничения


Как описано выше, проверка предоставленного сервером сертификата на валидность для хоста, указанного в используемом вами URL, при атаке «человек посередине» не позволяет просто подсунуть любой валидный сертификат, купленный или полученный нелегально. Это один из четырёх неотъемлемых шагов, позволяющих сделать ваше соединение полностью безопасным.


Система выполняет проверку благодаря параметру CN_match, объявляемому SSL Context в HTTPS-обёртке PHP. Но у него есть недостаток. На момент написания книги проверялось лишь общее наименование (Common Name, CN) SSL-сертификата, а валидность определяемого сертификатом поля «дополнительное имя субъекта» (Subject Alternative Name, SAN) не проверялась. SAN позволяет защищать несколько имён доменов с помощью одного SSL-сертификата, поэтому оно крайне важно и поддерживается всеми современными браузерами. А поскольку PHP пока что не поддерживает проверку SAN, то SSL/TLS-подключения к домену, защищённому таким сертификатом, установлены не будут. Поддержка SAN появится в PHP с версии 5.6.


C другой стороны, расширение cURL из коробки поддерживает SAN, так что его использование гораздо надёжнее и предпочтительнее по сравнению со встроенными в PHP HTTPS/FTPS-обёртками. В связи с этим применение PHP-потоков с большой вероятностью может привести к ошибочному поведению, и нетерпеливые программисты просто отключат проверку хоста в целом, что крайне нежелательно делать.


SSL Context в PHP-сокетах


Многие HTTP-клиенты в PHP предлагают и cURL-адаптер, и используемый по умолчанию PHP-сокет адаптер. Применение последнего по умолчанию говорит о том, что cURL — опциональное расширение, на практике его можно отключить.


PHP-сокеты используют тот же ресурс SSL Context, что и PHP-потоки, поэтому здесь присутствуют те же проблемы и ограничения, что описаны выше. Побочный эффект: многие основные HTTP-клиенты, скорее всего, априори ненадёжны и менее безопасны, чем должны быть. Подобные клиентские библиотеки, когда это возможно, необходимо конфигурировать так, чтобы они использовали cURL-адаптер. Также проверяйте, чтобы клиенты не забывали применять правильный подход к обеспечению безопасности SSL/TLS.


Дополнительные риски


Расширение cURL


В отличие от PHP-потоков, расширение cURL лишь передаёт данные, в том числе HTTP-запросы. Также, в отличие от SSL Context потоков, cURL по умолчанию безопасно выполняет запросы через SSL/TLS. Вам для этого ничего не нужно делать, если только оно не было скомпилировано без хранилища пакета CA-сертификатов (например, без файла cert.pem или ca-bundle.crt с сертификатами надёжных CA).


Поскольку это не требует особого подхода, вы можете вызвать Twitter API — аналогично тому, как мы делали это раньше для SSL/TLS с использованием PHP-потока. Минимум хлопот, и не надо беспокоиться о том, что вы забудете о каких-то опциях, которые откроют вас для атаки «человек посередине».


$url = 'https://api.twitter.com/1/statuses/public_timeline.json';
$req = curl_init($url);
curl_setopt($req, CURLOPT_RETURNTRANSFER, TRUE);
$result = curl_exec($req);

Поэтому для HTTPS-запросов рекомендую cURL. По умолчанию это безопасно, а PHP-потоки — наверняка нет. В противном случае просто используйте cURL, это избавит вас от головной боли. В конечном счёте cURL безопаснее, требует меньше кода и с меньшей вероятностью подвержен сбоям в безопасности SSL/TLS по вине ошибок человека.


На момент написания книги PHP 5.6 достиг версии alpha1. В финальном релизе появятся более безопасные значения по умолчанию для PHP-потоков и сокет-соединений через SSL/TLS. Эти нововведения не будут портированы в PHP 5.3, 5.4 или 5.5. Поэтому программистам придётся сознательно внедрять безопасные настройки по умолчанию, пока PHP 5.6 не превратится в необходимый минимум.


Конечно, если расширение cURL было включено без настройки размещения пакета надёжных сертификатов, то вышеприведённый пример работать не будет. Для публично распространяемых библиотек нужно принудительно настраивать безопасное поведение:


$url = 'https://api.twitter.com/1/statuses/public_timeline.json';
$req = curl_init($url);
curl_setopt($req, CURLOPT_RETURNTRANSFER, TRUE);
$result = curl_exec($req);

/**
 * Проверяет, является ли ошибка сбоем SSL, повторяет попытку с помощью пакета 
 * CA-сертификатов, предполагая, что локальный сервер не сконфигурирован 
 * для ext/curl. Ошибка 77 ссылается на CURLE_SSL_CACERT_BADFILE, который 
 * по какой-то причине не определён в качестве константы в мануале PHP.
*/
$error = curl_errno($req);
if ($error == CURLE_SSL_PEER_CERTIFICATE || $error == CURLE_SSL_CACERT
|| $error == 77) {
    curl_setopt($req, CURLOPT_CAINFO, __DIR__ . '/cert-bundle.crt');
    $result = curl_exec($req);
}

/**
 * Все последующие ошибки нельзя исправить без нарушения безопасности.
 * Поэтому не пытайтесь отключить SSL и попробовать снова ;).
 */

Самая сложная часть, очевидно, распространение файла с пакетом сертификатов cert-bundle.crt или cafile.pem (имя файла варьируется в зависимости от источника!). Сертификат любого CA может быть в любое время аннулирован большинством браузеров в случае нарушения их безопасности или процессов рецензирования (peer review), так что долго не обновлять файл с сертификатами — не лучшая идея. Тем не менее самое очевидное решение — распространять копию файла с библиотекой или приложением, для которого он требуется.


Если вы не можете строго контролировать обновление распространяемого пакета сертификатов или вам нужен инструмент, способный периодически выполнять за вас эту проверку, воспользуйтесь Sslurp.


SSL/TLS-соединения со стороны клиента (клиент/браузер-сервер)


Большая часть того, о чём мы говорили до этого, была связана с SSL/TLS-соединениями, устанавливаемыми с другим сервером по инициативе PHP веб-приложения. Конечно, есть ещё ряд проблем в безопасности, когда приложение оказывает SSL/TLS-поддержку клиентским браузерам и другим приложениям. Здесь возникает риск атак, связанных с уязвимостями защиты на транспортном уровне.


Если вдуматься, то всё это довольно просто. Скажем, я создаю онлайн-приложение, обеспечивающее защиту при вводе пользовательского пароля. Форма авторизации обслуживается через HTTPS, и данные из неё тоже передаются через HTTPS. Миссия выполнена. Чтобы начать работу в своём аккаунте, пользователь был перенаправлен на HTTP URL. Заметили проблему?


Когда есть угроза атаки «человек посередине», мы не должны просто защищать форму авторизации, а потом её закрывать. Куки пользовательских сессий и все вводимые данные, а также весь получаемый пользователями HTML-код не будут в безопасности при работе через HTTP. Злоумышленник может украсть куки и выдать себя за пользователя, может внедрить XSS-код в получаемые пользователями страницы для выполнения задач от лица пользователей или управления их действиями. И для всего этого не нужны пользовательские пароли.


Одна лишь защита процесса аутентификации с помощью HTTPS предотвратит прямую кражу пароля, но не защитит от перехвата сессии, других форм кражи данных, а также от внедрения кода для XSS-атак. Защищая с помощью HTTPS одного лишь пользователя, мы обеспечиваем недостаточную защиту на транспортном уровне. Наши пользователи остаются уязвимы к атакам «человек посередине».

Комментарии (0)