Привет.
Это – короткая статья о вспомогательном техническом средстве, нужном, чтобы администратор получал обратную реакцию подключающихся браузеров клиентов на различные применяемые технологии web-безопасности.
Этих технологий сейчас достаточно много, и если внедрить все скопом, и при этом не получать сообщения вида “я браузер, подключаюсь к вашему серверу, а он что-то непонятное/неправильное сообщает” – эффект будет куда как ниже, а то и будет обратным.
Поговорим про параметр report-uri и обработку поступающих на указанный в нём адрес данных.
- Использование report-uri для HPKP – HTTP Public Key Pinning
- Использование report-uri для DNS CAA – DNS Certification Authority Authorization
- Использование report-uri для HTTP-заголовка Expect-Staple
- Использование report-uri для HTTP-заголовка Expect-CT
- Использование report-uri и report-to для HTTP-заголовка Content-Security-Policy
- Пример реализации локального report-uri на PHP
Подсистемы, которые используют report-uri
Зачем столько внимания какому-то одиночному параметру? Посмотрим, где он используется.
Использование report-uri для HPKP – HTTP Public Key Pinning
Механизм HTTP Public Key Pinning я разбирал в статье про настройку TLS, поэтому не буду повторяться.
Скажем просто – если браузер клиента распознает заголовок HPKP и при этом получит сертификат, хэш ключа которого не будет соответствовать ни одному из вариантов из заголовка HPKP, то где-то “по пути” идёт попытка перехвата сессии – и вместо настоящего сертификата узла подставляется доверенный, но другой. Вашу сессию кто-то “разворачивает”, читает, снова “заворачивает” в TLS и пытается остаться незамеченным. В этом случае клиентский браузер, если задан report-uri
, уведомит сервер, к которому подключается, что произошла попытка подставить сертификат не указанный в перечне HPKP-заголовка. Эта штука работает примерно так же, как Certificate Pinning в EMET, только сообщает не клиенту “нам подставляют сертификат того же сервера, но с другим ключом”, а серверу – “подключающегося к нам клиента хотят обмануть”. Со стороны сервера это знать важно и нужно, так как можно придумать схему оповещения клиента вида “у вас показывается, что вы к нам подключились по TLS, но на самом деле сессия не является безопасной”, ну либо просто учитывать, что в данной сессии нельзя передавать sensitive data.
Использование report-uri для DNS CAA – DNS Certification Authority Authorization
Механизм DNS Certification Authority Authorization также расписан в отдельной статье и будет нужен, чтобы в явном виде задекларировать для внешних клиентов то, какими CA вы пользуетесь; это отсечёт атаки вида “у нас всегда сертификат Let’s Encrypt, но вот тут на днях взломали Comodo и выпустили от их имени сертификат с нашим FQDN; сертификат этот по всем возможным критериям доверенный, и что делать – непонятно”, потому что у вас в DNS будет явно указано “мы используем только явно указанные CA и никакие другие”.
Если задан report-uri
, то CA, которому уведомит сервер, к которому подключается, что произошла попытка подставить не подходящий под перечень в HPKP-заголовке сертификат.
Использование report-uri для HTTP-заголовка Expect-Staple
Про заголовок HTTP, нужный для декларирования OCSP Stapling’а, есть отдельная статья про настройку OCSP и связанных с ним технологий. Нас будет интересовать именно отчётность – в данном случае report-uri
используется, чтобы сообщить серверу о том, что в сертификате со стороны сервера есть OID Must-Staple, т.е. сервер подписывается что “ты не ходи за OCSP сам – я тебе с ответом верну, я уже всё сделал за тебя”, но не выполняет этого.
Использование report-uri для HTTP-заголовка Expect-CT
Expect-CT – это заголовок HTTP, нужный для декларирования клиенту, что используется механизм Certificate Transparency – то есть сервер получил сертификат через публично журналируемый CA, и запись о выдаче данного сертификата есть и доступна. В данном случае report-uri
используется, чтобы сообщить серверу о том, что заявление “да хоть где проверь мой сертификат, мне нечего скрывать” есть, а по факту сертификата на crt.sh – нет.
Использование report-uri и report-to для HTTP-заголовка Content-Security-Policy
Про Content-Security-Policy можно прочитать на официальном сайте W3C. Что интересно, вариант report-uri
помечен как deprecated и надо использовать report-to
– но по факту лучше указывать оба. По крайней мере на данный момент – возможно, через некоторое время все браузеры начнут уметь report-to
.
Как и в предыдущих примерах, по этому адресу будут отправляться отчёты о нарушении политики безопасности – в варианте Content-Security-Policy этих нарушений может быть множество, так что отслеживать их обязательно нужно.
Пример реализации локального report-uri на PHP
В большинстве случаев администраторы просто кладут на всё это и не задают report-uri
. Ну или задают только “почтовый” вариант, с mailto:
. Увы, браузеры не любят писать почту, и не найдя JSON-вариант report-uri
будут молча игнорировать ошибку и вести себя так, как считают нужным. Ну а администратор просто не будет знать о том, что это происходит, и что его подсистемы безопасности и связанные с ними технологии – просто не работают.
Самый простой способ – использовать внешний сервис, тот же report-uri.com. Он бесплатен для небольших количеств доменов и отчётности.
Однако, всё же отчитываться куда-то наружу, притом о таких серьёзных штуках – не самый лучший вариант.
Можно сделать и локально. В моём примере я написал мелкий скрипт на PHP (используется версия 7.2), который поместил в служебный подкаталог на сайте www.atraining.ru
, назвав его просто – report-uri
. За основу взят скрипт тов. Shaun C..
Внимание! Скрипт не предлагается использовать в production – хотя я использую. Напишите свой. Этот – для примера.
Так как у меня для каждого типа отчётов свой report-uri
– для DNS CAA, например, https://www.atraining.ru/report-uri/caa
, для HTTP Public Key Pinning – https://www.atraining.ru/report-uri/hpkp
, то нужен будет один PHP-файл, в котором весь функционал, ну а приём запросов можно будет реализовать через N небольших файлов, отличающихся лишь деталями для каждого из вариантов report-uri
.
Первым делом – надо научиться обрабатывать входящие HTTP-заголовки. Для этого в PHP есть функция getallheaders(), но она является alias’ом к Apache-реализации, а у меня nginx или tengine, который тоже nginx.
Сделаем просто:
function nginx_getallheaders() { $headers = []; foreach ($_SERVER as $name => $value) { if (substr($name, 0, 5) == 'HTTP_') { $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; } } return $headers; }
Теперь надо сделать предобработку запроса – если нам вместо POST пришёл OPTIONS, ответим клиенту, что мы всё ж умеем принимать и POST тоже – т.е. нам можно и нужно слать инфу. Это делается для ряда клиентов, которые специфично обрабатывают report-uri
:
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { header('Access-Control-Allow-Methods: OPTIONS, POST'); foreach (nginx_getallheaders() as $key=>$val) { if (strcasecmp($key, 'Access-Control-Request-Headers') == 0) { header('Access-Control-Allow-Headers:'.htmlentities($val, ENT_QUOTES)); break; } } }
Окей, ну а теперь сам парсинг:
if ($_SERVER['REQUEST_METHOD'] == 'POST' && strlen($json = @file_get_contents('php://input')) > 0) { $report = print_r(json_decode($json, true), true); $headers = print_r(nginx_getallheaders(), true); $res = функция_записи_в_БД(название_источника, $headers, $report); }
Я вывожу результаты в переменную через print_r
, потому что все равно потом их доп.обрабатываю специфичной функцией, помещающей результат в БД. Вы можете делать как-то иначе – всё зависит от того, как будет идти обработка. Если хотите отправлять себе каждый report отдельным письмом – можете сделать что-то типа:
mail('ваша@почта', '['.$_SERVER['SERVER_NAME'].'] Report-URI уполномочен сообщить', $body, 'From: ваша@почта');
Мне проще помещать всё в БД – но тут опять же, по ситуации.
Сопоставив эти куски кода в один файл, и подставив вместо функция_записи_в_БД свою функцию записи в таблицу “отчёты” вашей БД, а вместо название_источника поставив что-то, идентифицирующее отправляющую систему – например, HPKP или Expect-CT – вы сможете обрабатывать входящие уведомления report-uri.
Вкратце всё – и, повторюсь – код дан просто для примера того, что реализовать приём HTTP-запроса с базовым парсингом – элементарно и совершенно необязательно для этого использовать отдельный внешний сервис.
Удачного использования!