Пароль может не попасть в базу в открытом виде, не утечь из хранилища хешей — и всё равно оказаться в открытом виде в файле журнала, доступном гораздо более широкому кругу людей. Разбираем, какие именно данные считаются недопустимыми в логах, через какие каналы секрет туда просачивается и в какой точке цепочки логирования его можно вырезать.
Что относится к данным, которые нельзя писать в журнал
OWASP Logging Cheat Sheet выделяет категорию «Data to exclude» — данные, которые обычно не должны записываться в логи напрямую, а вместо этого должны быть удалены, замаскированы, очищены, хешированы или зашифрованы. В неё входят:
- исходный код приложения;
- значения идентификаторов сессий (при необходимости отслеживать события конкретной сессии их можно заменить хешированным значением);
- токены доступа;
- чувствительные персональные данные и некоторые формы PIIpersonally identifiable information — сведения, позволяющие идентифицировать человека (например, здоровье, государственные идентификаторы, уязвимые категории людей);
- пароли аутентификации;
- строки подключения к базе данных;
- ключи шифрования и другие первичные секреты.
Пароли аутентификации указаны отдельным пунктом — «Authentication passwords» — без привязки к конкретному полю формы. Именно поэтому неважно, в каком поле формы оказался пароль: в списке он назван как самостоятельная категория, а не как «значение поля password».
Неуспешный вход логировать нужно — но не целиком
Неуспешные попытки аутентификации попадают в список событий, которые следует логировать всегда: Authentication successes and failuresсобытия успешной и неуспешной аутентификации, которые OWASP Logging Cheat Sheet относит к обязательным для логирования. Для каждого события журнал должен фиксировать «when, where, who and what». Среди атрибутов, которые рекомендуется записывать, — тип события, серьёзность, описание, действие (например, вход), объект, статус результата (Success, Fail, Defer) и причину (например, Incorrect credentialsпричина отказа во входе, когда введённые учётные данные не совпали с ожидаемыми). Статус результата показывает, было ли действие успешным: Success — успех, Fail — неудача, Defer — отложенный результат, когда действие ещё не завершено и не отклонено окончательно. Также в список входят идентификатор взаимодействия, адрес источника, идентичность пользователя, код HTTP-статуса и HTTP-заголовки/User-Agent.
При этом сами пароли аутентификации в этот набор не входят: они остаются в категории данных, которые нельзя писать напрямую. То есть событие «попытка входа не удалась» фиксируется подробно, а значение введённого пароля — нет.
Хеширование на стороне хранения тут не помогает
Password Storage Cheat Sheet прямо не описывает момент, когда пароль ещё не хеширован, как отдельную стадию. Зато он говорит о свойствах самих преобразований:
- шифрование — двусторонняя функция, поэтому злоумышленники могут восстановить исходный открытый текст из зашифрованных данных; шифрование паролей допустимо лишь в исключительных случаях, когда нужно получить исходный пароль в открытом виде;
- сильные пароли, хранимые современными алгоритмами хеширования с соблюдением лучших практик, должны быть практически невзламываемы, но атакующий всё равно может «взломать» хеш, выбрав предполагаемый пароль, вычислив его хеш и сравнив с хешем жертвы.
Work factorчисло итераций алгоритма хеширования, обычно 2^work; хранится в выводе хеша и делает вычисление более затратным задаёт баланс между безопасностью и производительностью: слишком высокий замедляет проверку попытки входа и может привести к деградации производительности, которую злоумышленник использует для denial of serviceотказа в обслуживании за счёт исчерпания CPU сервера множеством попыток входа.
Ключевое здесь то, что все эти механизмы защищают хранилище хешей. Про утечку через логи в 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раздел RFC 9110 о конфиденциальности журнальной информации серверов описывает механику накопления: сервер может сохранять персональные данные о запросах пользователя с течением времени, что выявляет его читательские шаблоны или предметы интереса. Журнальная информация, собранная на посредникеузле, через который проходит запрос по цепочке соединений между клиентом и сервером, часто содержит историю взаимодействия пользовательского агента по множеству сайтов, которую можно проследить до отдельных пользователей.
Почему параметры в строке запроса считаются раскрытыми: 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часть authority URI, предназначенная для включения данных аутентификации пользователя. Причина: некоторые реализации используют 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, указывающий протокол и версию протокола, использованные вышестоящим отправителем сообщения указывает протокол и версию, использованные вышестоящим отправителем, поэтому значение 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 и фильтры.
- Проверка «войти маркерным паролем и найти его во всех стоках» проверяет сразу и корректность логирования, и отсутствие нежелательных побочных эффектов.
- Ни один из рассмотренных документов не формулирует формального правила «пароль в логе = скомпрометирован». Но раз журнал даёт атакующему менее защищённый путь к данным, а доказать отсутствие копии у имевшего доступ нельзя, обращаться с таким паролем как с утёкшим — единственная разумная позиция.