Современный feedback от web-систем: настраиваем report-uri

Делаем локальный обработчик уведомлений report-uri

Привет.

Это – короткая статья о вспомогательном техническом средстве, нужном, чтобы администратор получал обратную реакцию подключающихся браузеров клиентов на различные применяемые технологии web-безопасности.

Этих технологий сейчас достаточно много, и если внедрить все скопом, и при этом не получать сообщения вида “я браузер, подключаюсь к вашему серверу, а он что-то непонятное/неправильное сообщает” – эффект будет куда как ниже, а то и будет обратным.

Поговорим про параметр report-uri и обработку поступающих на указанный в нём адрес данных.

Подсистемы, которые используют 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-запроса с базовым парсингом – элементарно и совершенно необязательно для этого использовать отдельный внешний сервис.

Удачного использования!

Вернуться к полному списку статей Knowledge Base @ Advanced Training

Ruslan V. Karmanov

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