Назад к блогу

Пароль в логах: как учётные данные попадают в журналы приложений и что с этим делать

Пароль в логах: как учётные данные попадают в журналы приложений и что с этим делать

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

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

Что относится к данным, которые нельзя писать в журнал

OWASP Logging Cheat Sheet выделяет категорию «Data to exclude» — данные, которые обычно не должны записываться в логи напрямую, а вместо этого должны быть удалены, замаскированы, очищены, хешированы или зашифрованы. В неё входят:

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

Пароли аутентификации указаны отдельным пунктом — «Authentication passwords» — без привязки к конкретному полю формы. Именно поэтому неважно, в каком поле формы оказался пароль: в списке он назван как самостоятельная категория, а не как «значение поля password».

Неуспешный вход логировать нужно — но не целиком

Неуспешные попытки аутентификации попадают в список событий, которые следует логировать всегда: Authentication successes and failures. Для каждого события журнал должен фиксировать «when, where, who and what». Среди атрибутов, которые рекомендуется записывать, — тип события, серьёзность, описание, действие (например, вход), объект, статус результата (Success, Fail, Defer) и причину (например, Incorrect credentials). Статус результата показывает, было ли действие успешным: Success — успех, Fail — неудача, Defer — отложенный результат, когда действие ещё не завершено и не отклонено окончательно. Также в список входят идентификатор взаимодействия, адрес источника, идентичность пользователя, код HTTP-статуса и HTTP-заголовки/User-Agent.

При этом сами пароли аутентификации в этот набор не входят: они остаются в категории данных, которые нельзя писать напрямую. То есть событие «попытка входа не удалась» фиксируется подробно, а значение введённого пароля — нет.

Хеширование на стороне хранения тут не помогает

Password Storage Cheat Sheet прямо не описывает момент, когда пароль ещё не хеширован, как отдельную стадию. Зато он говорит о свойствах самих преобразований:

  • шифрование — двусторонняя функция, поэтому злоумышленники могут восстановить исходный открытый текст из зашифрованных данных; шифрование паролей допустимо лишь в исключительных случаях, когда нужно получить исходный пароль в открытом виде;
  • сильные пароли, хранимые современными алгоритмами хеширования с соблюдением лучших практик, должны быть практически невзламываемы, но атакующий всё равно может «взломать» хеш, выбрав предполагаемый пароль, вычислив его хеш и сравнив с хешем жертвы.

Work factor задаёт баланс между безопасностью и производительностью: слишком высокий замедляет проверку попытки входа и может привести к деградации производительности, которую злоумышленник использует для denial of service.

Ключевое здесь то, что все эти механизмы защищают хранилище хешей. Про утечку через логи в Password Storage Cheat Sheet не говорится вовсе — это предмет Logging Cheat Sheet, где пароли аутентификации отнесены к данным, которые нельзя писать напрямую. Корректно настроенный work factor не спасает от того, что открытый пароль оказался в текстовом файле.

Что именно теряется при утечке через журнал

CWE-532 («Insertion of Sensitive Information into Log File») описывает слабость так: продукт записывает чувствительную информацию в лог-файл. Последствие одно — нарушение конфиденциальности при чтении данных приложения: запись в журнал чувствительных данных, полных путей или системной информации даёт атакующим дополнительный, менее защищённый путь к получению информации.

Меры, которые CWE-532 предлагает: серьёзно оценивать чувствительность записываемой информации, не писать секреты в журналы, удалять отладочные журналы перед развёртыванием в продакшене, защищать журналы от несанкционированного чтения/записи и корректировать конфигурации при переходе от отладки к продакшену.

RFC 9110 добавляет требование хранить журнальную информацию безопасно и очищать её от персонально идентифицируемой информации — включая идентификаторы пользователей, IP-адреса и пользовательские параметры запросов — как только она перестаёт быть нужной. Но ни один из источников не формулирует требование считать пароль скомпрометированным после попадания в журнал. Это важно: формального правила «попал в лог — считай утёкшим» в рассмотренных документах нет, хотя практические последствия именно таковы.

Журнал доступа: всё, что попало в URL, сохраняется

RFC 9110 §17.8 Privacy of Server Log Information описывает механику накопления: сервер может сохранять персональные данные о запросах пользователя с течением времени, что выявляет его читательские шаблоны или предметы интереса. Журнальная информация, собранная на посреднике, часто содержит историю взаимодействия пользовательского агента по множеству сайтов, которую можно проследить до отдельных пользователей.

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

Структура URI задана ABNF-правилами:

http-URI  = "http"  "://" authority path-abempty [ "?" query ]
https-URI = "https" "://" authority path-abempty [ "?" query ]

Схема и хост регистронезависимы и обычно записываются в нижнем регистре, остальные компоненты сравниваются с учётом регистра. Символы вне набора «reserved» эквивалентны своим percent-encoded октетам, нормальная форма — не кодировать их. Многие узлы логируют или отображают target URI в видимых третьим лицам местах, а логи следует очищать от персонально идентифицируемой информации, как только она перестанет быть нужной.

Учётные данные в userinfo и Referer

RFC 9110 §4.2.4 объявляет устаревшим формат «user:password» в подкомпоненте userinfo. Причина: некоторые реализации используют userinfo для внутренней конфигурации аутентификации (в параметрах команд, конфигурационных файлах, списках закладок), что может раскрыть идентификатор пользователя или пароль.

Правила поведения жёсткие: отправитель не должен генерировать userinfo при создании http/https URI в сообщении как target URI или значение поля. Получатель, принимая URI из недоверенного источника, должен разобрать его на наличие userinfo и считать это ошибкой — вероятно, так пытаются скрыть authority в фишинговых атаках.

С журналами это связано напрямую: URI часто записываются или отображаются серверами, прокси и агентами пользователя в видимых третьим лицам местах, поэтому включать в URI чувствительную информацию неразумно. Риск попадания в Referer снижается отдельным требованием: агент пользователя не должен включать фрагмент и компоненты userinfo URI при формировании значения поля Referer.

Один запрос — несколько записей у разных узлов

HTTP позволяет использовать посредников; есть три распространённые формы: proxy (proxy), gateway (gateway) и tunnel (tunnel), причём один посредник может выступать в разных ролях в зависимости от характера запроса.

Поле заголовка Via указывает на наличие промежуточных протоколов и получателей между пользовательским агентом и сервером (в запросах) или между исходным сервером и клиентом (в ответах). Оно служит для отслеживания пересылки сообщений, предотвращения петель запросов и определения протокольных возможностей отправителей вдоль цепочки запрос/ответ. Каждый элемент значения Via представляет прокси или шлюз, переславший сообщение; каждый посредник добавляет свою информацию о том, как сообщение было получено, так что итог упорядочен по последовательности пересылающих получателей. Прокси обязан отправлять подходящее поле Via в каждом пересылаемом сообщении, а HTTP-to-HTTP шлюз — в каждом входящем запросе и может отправлять его в пересылаемых ответах.

Для каждого посредника received-protocol указывает протокол и версию, использованные вышестоящим отправителем, поэтому значение Via фиксирует объявленные протокольные возможности цепочки, делая их видимыми для нижестоящих получателей. Часть received-by обычно представляет собой хост и необязательный номер порта получателя, который переслал сообщение; если реальный хост считается конфиденциальной информацией, отправитель может заменить его псевдонимом.

Практический вывод: запрос с секретом в URI или заголовке оставляет запись на каждом узле цепочки, и каждая такая запись — отдельная точка утечки.

Уровни логирования и что в них попадает

OWASP Logging Cheat Sheet не задаёт фиксированных уровней для продакшена, а говорит о настраиваемом уровне: тип событий по серьёзности или уровню угрозы, объём записываемых деталей. При этом уровень по умолчанию должен давать достаточно деталей для бизнес-нужд, и нельзя полностью отключать логирование приложения или событий, необходимых для соответствия требованиям.

Всегда логировать следует:

  • успехи и неудачи аутентификации;
  • отказы авторизации (контроля доступа);
  • ошибки приложения и системные события (синтаксические и runtime-ошибки, проблемы связности, производительности, сообщения сторонних сервисов, ошибки файловой системы, обнаружение вирусов при загрузке файлов, изменения конфигурации).

Расширенные детали — трассировку стека, тела HTTP-запросов и заголовки/тела ответов — рекомендуется выносить в отдельные файлы или таблицы. Именно в этих расширенных деталях и оседают секреты, если их туда пустить.

Заголовок Authorization

Поле Authorization позволяет агенту пользователя аутентифицироваться на сервере-источнике, обычно после ответа 401 (Unauthorized). Его значение состоит из credentials — аутентификационной информации агента для realm запрашиваемого ресурса. Синтаксис:

Authorization = credentials
credentials   = auth-scheme [ 1*SP ( token68 / #auth-param ) ]

Здесь auth-scheme — имя схемы аутентификации, token68 — компактная строка учётных данных, а auth-param — именованный параметр схемы.

Агент, желающий аутентифицироваться, включает это поле в запрос; значение содержит credentials клиента для realm ресурса, основанные на полученном ранее challenge. Передача credentials внутри значений полей заголовков влечёт значительные риски для конфиденциальности соединения.

Поскольку credentials — это аутентификационная информация, её попадание в лог (например, при отладочном логировании HTTP-клиента) означает, что секрет записан в открытом виде. OWASP относит к данным, которые нельзя писать напрямую, в частности Authentication passwords и Access tokens.

Цепочка логирования в Python: где вырезать секреты

В Python logging поток данных устроен так:

  • код приложения работает с Logger;
  • Handler доставляет записи до цели;
  • Фильтр решает, что пропустить;
  • Formatter превращает запись в строку.

Сообщения, записанные в модульный логгер, пересылаются обработчикам логгеров верхних уровней вплоть до root logger — это иерархическое логирование. Централизованно вырезать секреты до записи в файл можно на уровне Handler: у него есть метод addFilter, а сам он отправляет записи в место назначения, то есть фильтр на обработчике отсекает записи прежде, чем они попадут в цель.

Перехват значений до формирования строки

LogRecord создаётся автоматически Logger при каждом логировании и содержит всю информацию о событии. Его атрибут message вычисляется как msg % args и устанавливается при вызове Formatter.format(). То есть до момента форматирования запись ещё можно изменить.

Фабрику записей можно получить через logging.getLogRecordFactory() — она возвращает вызываемый объект, используемый для создания LogRecord, — и заменить через logging.setLogRecordFactory(). В примере старая фабрика сохраняется, обёртывается новой функцией, которая вызывает old_factory(args, *kwargs), добавляет к записи собственный атрибут и возвращает запись. Так значение поля задаётся до формирования итоговой строки.

LoggerAdapter служит для удобной передачи контекстной информации в вызовы логирования. Конструктор logging.LoggerAdapter(logger, extra=None, merge_extra=False) принимает нижележащий Logger, необязательный dict-подобный объект extra и флаг merge_extra, определяющий, объединять ли extra отдельного вызова с extra адаптера (по умолчанию extra отдельного вызова игнорируется). Метод process(msg, kwargs) изменяет сообщение и/или именованные аргументы вызова, добавляя объект extra в kwargs по ключу 'extra', и возвращает кортеж (msg, kwargs) с возможно изменёнными аргументами.

Дополнительно фильтры видят каждую запись, обрабатываемую обработчиком или логгером, к которому они привязаны, что позволяет добавлять, изменять или удалять атрибуты в обрабатываемом LogRecord и тем самым внедрять контекстную информацию в логи.

Централизованный сбор расширяет периметр

Раздел «Network architecture» описывает пример сервиса: приложения бизнес-логики размещены в сегментах FRONTEND 1 (DMZ, UI), MIDDLEWARE 1 (ядро сервиса) и BACKEND 1 (база данных сервиса). Сбор событий вынесен в отдельные сегменты: BACKEND 2 (хранилище журналов), MIDDLEWARE 3 с двумя приложениями — log loader (скачивает журналы из хранилища, предобрабатывает и передаёт в UI) и log collector (принимает журналы от бизнес-приложений, инфраструктуры и облачных приложений и сохраняет их в хранилище), FRONTEND 2 (UI для просмотра журналов) и FRONTEND 3 (приложения, принимающие журналы из облака и передающие их в log collector).

Рекомендуется создавать централизованную систему сбора журналов, при этом все сервисы должны безопасно собирать журналы в централизованной системе. На сетевом уровне процессы сохранения и скачивания журналов требуют открытия разных сетевых доступов (портов) и выполняются разными приложениями.

Почему ревью кода не ловит такие пути

CWE-532 описывает слабость как запись чувствительной информации в лог-файл. В приведённом примере сообщение об ошибке включает текст SQL-запроса, что раскрывает имена таблиц и столбцов: это происходит в блоке catch при обработке SQLException, где формируется logMessage с query и передаётся в логгер.

Для обнаружения этой слабости применяется автоматический статический анализ (SAST), который строит модель потоков данных и управления и ищет связи «источник — сток»: потенциально уязвимые шаблоны, соединяющие источники входных данных с местами, где данные взаимодействуют с внешними компонентами.

Что проверять

Раздел «Verification» предписывает включать логирование в код-ревью, тестирование и процессы проверки безопасности:

  • убедиться, что логирование работает корректно и как задано;
  • проверить, что события классифицируются согласованно, а имена, типы и длины полей определены по согласованному стандарту;
  • убедиться, что логирование включено при тестировании безопасности, фаззинге, пентестах и тестах производительности;
  • проверить, что механизмы не подвержены инъекционным атакам;
  • убедиться в отсутствии нежелательных побочных эффектов;
  • проверить поведение при потере внешней сетевой связности, если она обычно требуется;
  • убедиться, что логирование нельзя использовать для исчерпания ресурсов (заполнение диска или журнала транзакций БД, ведущее к отказу в обслуживании).

Отдельно «Data to exclude» требует, чтобы перечисленные данные обычно не записывались напрямую, а удалялись, маскировались, санировались, хешировались или шифровались. Санитизация всех данных события выполняется в том числе для предотвращения инъекций через журнал (возврат каретки, перевод строки, символы-разделители) и опционально — чтобы удалить чувствительные данные.

Тест «войти маркерным паролем и найти его во всех стоках» соотносится с этими требованиями так: он проверяет, что пароль аутентификации не попадает в логи, то есть что выполняется пункт о недопустимости прямой записи паролей и что санитизация данных события действительно удаляет чувствительные данные. Кроме того, такой тест относится к проверке «Ensure the logging is working correctly and as specified» и к проверке отсутствия нежелательных побочных эффектов, поскольку ищет секрет во всех местах вывода логов.

Целостность журналов и мониторинг

Раздел «Integrity» ставит вопрос, какая информация и кем может изменяться, и учитывает атаки, при которых злоумышленник с доступом на чтение использует журнал для похищения секретов или эксплуатирует платформу логирования. Для защиты предписано:

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

Раздел «Monitoring of events» требует, чтобы данные событий были доступны для просмотра и существовали процессы мониторинга, оповещения и отчётности: встраивание журналирования приложения в системы централизованного сбора и анализа, доступность информации нужным командам, немедленные оповещения ответственных команд о серьёзных событиях и обмен данными с другими системами обнаружения.

Реакция на компрометацию

Password Storage Cheat Sheet в разделе «Upgrading Legacy Hashes» описывает обновление устаревших хешей так: когда пользователь вводит пароль (обычно при аутентификации), этот ввод следует повторно хешировать новым алгоритмом, а защитники должны сделать текущий пароль недействительным и потребовать ввести новый, чтобы старые (менее безопасные) хеши пароля больше не были полезны атакующему. Поскольку при этом старые хеши остаются в базе до входа пользователя, предлагаются два подхода:

  • истечь и удалить хеши давно неактивных пользователей и потребовать сброса пароля для входа;
  • использовать существующие хеши как вход для более безопасного алгоритма, например bcrypt(md5($password)), с заменой на прямые хеши при следующем входе.

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

Что из этого следует на практике

  • Пароль в логе — это открытый текст, независимо от того, насколько хорош work factor в хранилище хешей. Хеширование защищает базу, а не журнал.
  • Пароли аутентификации — отдельная категория данных, а не «значение поля password». Значит, фильтровать по имени поля недостаточно: нужно вырезать сам класс данных.
  • Секрет в URI или заголовке Authorization оставляет запись на каждом узле цепочки посредников. Одна утечка размножается по числу узлов.
  • Централизованный сбор журналов расширяет периметр: копия секрета оказывается в хранилище, доступном отдельным приложениям и командам.
  • Отладочные детали — трассировки, тела запросов, заголовки ответов — выносятся в отдельные файлы или таблицы, и именно там секреты оседают чаще всего.
  • В Python logging вырезать секреты централизованно можно на уровне Handler через addFilter — до того, как запись попадёт в место назначения; дополнительно значения можно править через фабрику записей, LoggerAdapter и фильтры.
  • Проверка «войти маркерным паролем и найти его во всех стоках» проверяет сразу и корректность логирования, и отсутствие нежелательных побочных эффектов.
  • Ни один из рассмотренных документов не формулирует формального правила «пароль в логе = скомпрометирован». Но раз журнал даёт атакующему менее защищённый путь к данным, а доказать отсутствие копии у имевшего доступ нельзя, обращаться с таким паролем как с утёкшим — единственная разумная позиция.

Источники

Похожее