Привет, Хабр! Меня зовут Ноянова Наталья, около 10 лет я исследую и работаю в ИБ, сейчас занимаюсь статическим анализом кода, изучаю его недостатки и уязвимости. Один из типов таких недостатков кода привлек моё внимание. Анализаторы кода описывают это как внутренние или внешние утечки информации, которые связаны с журналированием ошибок работы программы. И правки этого недостатка кода навели меня на мысли, которые изложены в данной статье.
Для исправления подобных недостатков требуется не просто внести правки в код (вроде замены ссылок, начинающихся с «http» на «https»), а произвести целый ряд действий: перенастроить конфигурацию работы программы, понять суть безопасного логирования, найти баланс между необходимым и излишним. Ниже примеры таких решений, а также причины, почему это стоит вашего внимания. Сперва — общая теория, затем — обзор практик безопасного логирования, что будет, если вы его не настроите. Спойлер — сперва поймут, чем вы пользуетесь, затем подберут уязвимости для него, настроят эксплойты и применят.
Журналирование ошибок — это критически важная часть жизненного цикла программного обеспечения. Оно упрощает отладку, позволяет выявлять сбои на ранней стадии и предотвращать их повторение. Однако журналы (жарг. логи) должны быть не только информативными, но и безопасными.
Статья написана на основе горького опыта наблюдения за ошибками разработчиков и участия в CTF. Приведена общая информация о журналировании ошибок, библиотеках для этого, как это может сыграть на руку хакерам и как от этого можно предотвратить.
Основные принципы журналирования
Журналирование — это фиксация и структурирование информации о работе системы. Журналы делятся по значимости на уровни (по мере возрастания):
DEBUG: подробная информация для отладки (запуск сервера, запросы в БД. Это мы не используем на продовских серверах);
INFO: общие события работы сервиса (успешный вход пользователя);
WARNING: экстренные ситуации, проблемы, некорректные запросы;
ERROR: основные ошибки, при которых функция не выполнилась;
FATAL: критические ошибки, приводящие к остановке приложения.
Механизмы записи
Текстовые файлы: самый простой способ, каждая запись — отдельная строка.
Многоступенчатые журналы: сложная структура (например, стек вызовов), требующая для чтения специальных программ.
Бинарные журналы: обрабатываются тем же ПО, которое их записывает.
Базы данных: позволяют быстро искать, но могут замедлять работу СУБД при интенсивной записи.

Trace ID (идентификатор трассировки)
Это «скелет» всего запроса — уникальный идентификатор, который создаётся один раз на входе в систему (например, в API‑шлюзе) при поступлении запроса от пользователя. Он неизменен на протяжении всего жизненного цикла этого запроса, проходя через все микросервисы, базы данных и очереди сообщений. Отлично заменяет простой вывод трассы ошибки.
Какую задачу решает: собирает все события (спаны), связанные с конкретным вызовом пользователя, в единую цепочку (дерево). Это позволяет увидеть полную картину: сколько времени занял запрос, где возникла ошибка и какие сервисы участвовали.
Аналогия: номер дела в архиве. Все документы по этому делу, вне зависимости от того, в каком отделе они лежат, имеют один и тот же номер.
Correlation ID (идентификатор корреляции)
Идентификатор, связывающий события, которые логически относятся к одной бизнес‑операции, но могут состоять из нескольких технических запросов.
Может оставаться неизменным в рамках всей бизнес‑сессии или транзакции, даже если технически она разбита на множество отдельных запросов с разными trace_id.
Пример: пользователь нажал кнопку «Оформить заказ». Это действие запустило три разных HTTP‑запроса (проверка склада, списание денег, создание заказа). У каждого запроса будет уникальный trace_id, но у всех будет одинаковый correlation_id, чтобы мы могли понять, что эти три действия — часть одного заказа.
Какую задачу решает: связывает разрозненные технические события в единый бизнес‑контекст. Часто используется для отслеживания асинхронных процессов (например, когда запрос отправлен в очередь, а ответ придёт через час).
Аналогия: номер заказа клиента. Один заказ может включать в себя доставку, оплату и упаковку (разные процессы и запросы), но всё это объединено одним номером заказа.
Ключевые различия
Характеристика |
Trace ID |
Correlation ID |
Гранулярность |
Техническая (один HTTP‑запрос или один вызов метода). |
Бизнес‑уровень (целая транзакция, сессия, заказ). |
Отношение с запросами |
Не меняется в пределах одного запроса. |
Может охватывать множество запросов с разными |
Генерация |
Генерируется на входе каждого нового запроса. |
Часто передаётся клиентом или генерируется в начале бизнес‑процесса. |
Цель |
Отладка производительности и поиск ошибок в стеке вызовов. |
Отслеживание бизнес‑логики и состояния транзакции. |
Как они работают вместе?
В современных стандартах (например, W3C Trace Context) для технической трассировки часто используется именно trace_id. Однако в журналах и системах мониторинга (ELK, Splunk) очень удобно дублировать correlation_id (или использовать его как синоним trace_id в простых случаях), чтобы разработчики могли искать не только технические сбои, но и бизнес‑сценарии.
Пример сценария:
Клиент отправляет запрос «Купить товар».
Шлюз генерирует
trace_id: A1иcorrelation_id: Order-123.Сервис оплаты обращается к банку. У этого вызова новый
trace_id: B1, но он наследуетcorrelation_id: Order-123.Сервис склада обращается к базе данных. Новый
trace_id: C1, но тот жеcorrelation_id: Order-123.
Почему в примере не прошёл платеж? Смотрим trace_id: B1. Если нужно увидеть весь путь клиента от клика до получения товара, то ищем по correlation_id: Order-123.
Ротация файлов
Ротация — это архивирование старых журналов и удаление их для экономии места. Процесс включает в себя сохранение текущего журнала, переименование его файлов и создание нового журнала. Это критично для производительности системы.
Безопасность и защита логов
Безопасность журналов находится между категориями CWE-1210 «Ошибки аудита/ведения журналов» и CWE-200 “Раскрытие конфиденциальной информации посторонним лицам”.
С одной стороны, кажется, что ИБ хочется таким способом обезопасить все журналы, сделав их максимально обобщёнными и абстрактными, а то и вовсе их не вести. Но это не так. Журналы нужны для мониторинга событий и расследования инцидентов безопасности, поэтому существует такая категория недостатков как CWE-778 “Недостаточное журналирование”.
Поэтому постарайтесь, чтобы после возникновения проблем не получилась ситуация в двух актах:

Однако, с другой стороны, существуют такие категории как CWE-532 «Внесение в журнал конфиденциальной информации» (обычно в них речь идёт о паролях), CWE-538 «Размещение конфиденциальной информации в файле или каталоге, доступном извне» (тут речь о незашифрованных журналах, но консольное журналирование во фронтенд‑приложениях на JS подразумевает свободный просмотр этих журналов). Также необходимо помнить о CWE-117, связанном с инъекциями кода в журнал.
Мы подробнее будем рассматривать утечку информации через сообщения об ошибках (CWE-209) и CWE-1295 “Сообщения об ошибках, содержащие ненужную информацию. При эксплуатации этого недостатка атакующий может получить стек‑трассы, пути к файлам и версии библиотек.”
Примеры эксплуатации утечек информации через журналы
Вот несколько типичных сценариев и кода, демонстрирующего эту уязвимость:
Прямой вывод исключения в HTTP‑ответе (Flask, Django)
Самая частая ошибка — когда разработчик не перехватывает исключения глобально и фреймворк по умолчанию возвращает страницу отладки с полным стеком вызовов.
Уязвимый код (Flask):
from flask import Flask app = Flask(__name__) # В режиме отладки (DEBUG=True) Flask автоматически показывает стек-трейс # Но даже без этого, если не настроен обработчик, ошибка может «просочиться» @app.route('/divide') def divide(): a = 10 b = 0 # Это вызовет ZeroDivisionError result = a / b return str(result) if __name__ == '__main__': # Если запустить с debug=True, пользователь увидит полный стек-трейс app.run(debug=True)
Что видит атакующий:
Пользователь получает HTML‑страницу с красным текстом:
“C:\Program Files\Python310\python.exe” C:\Users\user\PycharmProjects\PythonProject\1.py
* Serving Flask app '1'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 391–274-997
Здесь указано:
Тип ошибки: ZeroDivisionError.
Полный стек вызовов:
File "/app/main.py", line 10, in divide.Путь к файлу на сервере: /app/main.py.
Версии библиотек (например, версия интерпретатора — Python3.10).
Это позволяет атакующему понять структуру приложения и версию среды выполнения.
Кстати, Debugger PIN — это секретный код для доступа к отладчику Werkzeug (находится в зависимостях Flask). То есть это просто дополнительный уровень безопасности на случай, если вы случайно запустите приложение в режиме DEBUG на рабочем сервере.

Ручной вывод traceback в ответе
Иногда разработчики пытаются обработать ошибку, но делают это небрежно, отправляя стек‑трассу прямо в ответ клиенту.
Уязвимый код:
import traceback from flask import Flask, request app = Flask(__name__) @app.route('/process') def process_data(): try: data = int(request.args.get('value')) # Логика, которая может упасть result = 100 / data return f"Результат: {result}" except Exception as e: # ОШИБКА: Мы отправляем стек-трейс пользователю error_details = traceback.format_exc() return f"Произошла ошибка: {error_details}", 500 if __name__ == '__main__': app.run(debug=False)
Последствия: в ответе придёт текст вроде:
Произошла ошибка: Traceback (most recent call last):
File “/var/www/my_app/app.py”, line 15, in process_data
result = 100 / data
ZeroDivisionError: division by zero
Атакующий узнает абсолютный путь к файлу (/var/www/my_app/app.py), что критично для подготовки атак типа LFI (Local File Inclusion) или для понимания структуры проекта, определения типа ОС исполняемого сервера (здесь, судя по направлению слешей, Linux).
Утечка через SQL‑инъекцию (CWE-89 + CWE-209)
Если база данных возвращает ошибку, а приложение просто пробрасывает её на фронтенд.
Уязвимый код:
import sqlite3 from flask import Flask, request app = Flask(__name__) @app.route('/user') def get_user(): username = request.args.get('username') conn = sqlite3.connect('users.db') cursor = conn.cursor() # Плохая практика: конкатенация строк query = f"SELECT * FROM users WHERE name = '{username}'" try: cursor.execute(query) return str(cursor.fetchall()) except sqlite3.Error as e: # ОШИБКА: Выводим сырую ошибку БД return f"Ошибка базы данных: {e}" if __name__ == '__main__': app.run(debug=False)
Что видит атакующий: если ввести ' OR '1'='1, то сервер может вернуть:
Ошибка базы данных: near “OR”: syntax error

Это подтверждает использование SQLite и помогает подобрать синтаксис для дальнейшей эксплуатации.

На момент написания этой статьи самые актуальные уязвимости связаны с SQL‑инъекциями исполняемого кода, направленными на переполнение буфера или вывод чувствительных данных. (выборка с https://www.opencve.io/, анализ OWASP top 10 за 2017–2025, БДУ ФСТЭК за этот период).
Как предотвратить утечку информации

Скрытие версий библиотек
Чтобы снизить риски атак на основе известных CVE, можно скрыть версии ПО:
HTTP‑заголовки: удалите
X-Powered-ByиServer. В Nginx используйтеserver_tokens off;.HTML‑код: удалите комментарии с версиями и используйте минификацию.
Пути к файлам: не используйте в URL версии (например,
/js/react@18.2.0/). Используйте хеши содержимого.WAF: настройте правила фильтрации ответов для удаления заголовков с информацией о версиях.
Полностью скрыть версии библиотек бывает сложно, особенно если они используются в клиентской части (браузере) и доступны через консоль разработчика (например, глобальные объекты window.React). В таких случаях главная цель — не дать эту информацию через HTTP‑заголовки, HTML‑комментарии и сообщения об ошибках, чтобы усложнить жизнь автоматическим сканерам уязвимостей.
Примеры кода
Вот несколько практических подходов и примеров кода.
Строгая проверка формата (регулярные выражения)
Самый надёжный способ — разрешить только определённые символы (обычно буквы, цифры и дефисы). Если в ID попадёт что‑то лишнее (например, перенос строки), то мы либо отклоним его, либо заменим на безопасный символ.
Пример на Python:
import re, uuid def safe_trace_id(user_input=None): # Если ID передан извне (например, из заголовка запроса) if user_input: # Проверяем, соответствует ли ID строгому формату UUID или алфавитно-цифровой строке. Разрешаем только буквы, цифры и дефисы if not re.match(r'^[a-zA-Z0-9\-]+$', user_input): # Если формат нарушен (есть спецсимволы), генерируем новый безопасный ID return str(uuid.uuid4()) return user_input else: # Генерируем новый ID, если входных данных нет return str(uuid.uuid4()) # Пример использования incoming_id = "req-123\nmalicious-injection" # Опасный ввод safe_id = safe_trace_id(incoming_id) print(f"Безопасный ID: {safe_id}") # Вывод: Будет сгенерирован новый UUID, так как исходный содержал '\n'
Экранирование перед записью (sanitization)
Если невозможно отклонить входящий ID, обязательно «очисти» его перед записью в журнал, заменив все управляющие символы на безопасные (например, подчеркивание _).
Пример на Python:
import re def sanitize_for_logging(value): # Заменяем переносы строк, табуляцию и другие управляющие символы # на безопасный символ '_' return re.sub(r'[\r\n\t\f\v]', '_', str(value)) # Пример использования raw_id = "trace-abc-123\r\n[INFO] Fake Log Entry" clean_id = sanitize_for_logging(raw_id) # Теперь можно безопасно записать в лог print(f"Log entry: Request ID: {clean_id}") # Результат: Log entry: Request ID: trace-abc-123_[INFO] Fake Log Entry # Запись останется в одной строке, инъекция предотвращена.
Использование стандартных библиотек трассировки
Такая защита уже встроена во многие современные фреймворки (например, OpenTelemetry). Они автоматически генерируют trace_id и span_id в формате hex‑строк, которые по определению не содержат опасных символов.
Пример логики:
Генерирование: используйте
uuid.uuid4()или библиотеку OpenTelemetry.Передача: отправляйте ID только через HTTP‑заголовки (например,
traceparentв стандарте W3C Trace Context).Запись: никогда не конкатенируйте ID с другими данными без проверки. Используйте структурированное журналирование (JSON), где каждый параметр — отдельное поле. Это автоматически изолирует значение ID от остального текста журнала.
Пример безопасного JSON‑журнала
"timestamp": "2026-06-01T13:22:00Z", "level": "INFO", "trace_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Request processed successfully"
В JSON‑формате даже если в trace_id случайно попадут спецсимволы, то парсер корректно их обработает, и они не сломают структуру файла.
Ещё несколько советов:
Глобальные обработчики исключений: перехватывайте все ошибки в одном месте.
Кастомные страницы ошибок: пользователь должен видеть только дружелюбное сообщение («Произошла ошибка»), без технических подробностей.
Журналирование вместо вывода: технические подробности (стек‑трассу) пишите только в защищённые файлы журналов на сервере.
Отключение отладки: в эксплуатации всегда устанавливайте
DEBUG=false.Маскировка данных: никогда не журналируйте пароли, токены и персональные данные.
-
Защита файлов журналов:
Права доступа: установите права
600(Linux) или ограничьте ACL (Windows). Доступ только у владельца и администратора.Изоляция: не храните журналы в корневой директории веб‑сервера.
Шифрование: используйте шифрование диска (LUKS, BitLocker) и TLS при передаче журналов в SIEM.
Централизация: Отправляйте журналы на отдельный защищённый сервер.
Чего делать НЕЛЬЗЯ
Не используйте пользовательские данные напрямую. Никогда не берите
trace_idиз параметров URL или тела запроса без валидации. Не пишите туда ФИО пользователя, девичью фамилию его матери, пароли и ключи шифрования. Не надо. Пожалуйста.Не игнорируйте отсутствие ID. Если заголовок с ID отсутствует, то всегда генерируйте новый. Не оставляйте поле пустым и не ставьте значение «unknown» без проверки, так как это может затруднить трассировку.
Не записывайте сырые строки. Избегайте конструкций вида
logger.info("ID: " + user_input). Всегда используйте параметры форматирования:logger.info("ID: %s", user_input).
Классификация ошибок и коды ответов
Хорошая практика обработки ошибок — присвоение им уникальных кодов для стандартизации. Вот самые популярные ошибки работы:
Код |
Категория ошибки |
Примеры исключений (Java, Kotlin, JS, Python) |
Описание |
0 |
Неизвестная ошибка |
Error |
Общая ошибка, не вызывается. Используется как шаблон |
1 |
Ошибка аргумента или парсинга |
IllegalArgumentException, NumberFormatException, ValueError, SyntaxError |
Недопустимое значение или формат данных. |
2 |
Ошибка ссылки или объекта |
NullPointerException, KeyError, IndexError, ReferenceError |
Попытка доступа к несуществующему объекту или ключу. |
3 |
Ошибка типа данных |
ClassCastException, TypeError, AttributeError, FileNotFoundException |
Невозможность приведения типа или операции над неподходящим типом. |
4 |
Ошибка диапазона или Состояния |
ArrayIndexOutOfBoundsException, IllegalStateException, RangeError |
Индекс вне границ или недопустимое состояние системы. |
5 |
Общая ошибка выполнения |
Exception, RuntimeError, Error |
Любая неклассифицированная ошибка. |
Реализация на языках программирования
Ниже приведены примеры кода для обработки ошибок с присвоением кодов 1–5.
Java:
import java.util.logging.Logger; import java.util.logging.Level; class ErrorInfo { private final int code; private final String message; private final String originalMessage; private final String stackTrace; public ErrorInfo(int code, String message, String originalMessage, String stackTrace) { this.code = code; this.message = message; this.originalMessage = originalMessage; this.stackTrace = stackTrace; } @Override public String toString() { return "ErrorInfo{code=" + code + ", message='" + message + "', originalMessage='" + originalMessage + "'}"; } } public static ErrorInfo handleError(Throwable error) { int errorCode = 0; String message = "Неизвестная ошибка"; String originalMessage = error.getMessage(); StringBuilder sb = new StringBuilder(); for (StackTraceElement element : error.getStackTrace()) { sb.append(element.toString()).append("\n"); } String stackTrace = sb.toString(); if (error instanceof IllegalArgumentException || error instanceof NumberFormatException) { errorCode = 1; message = "Ошибка аргумента: Передано недопустимое значение."; } else if (error instanceof NullPointerException) { errorCode = 2; message = "Ошибка ссылки: Попытка обращения к null-объекту."; } else if (error instanceof ClassCastException) { errorCode = 3; message = "Ошибка типа: Невозможно привести объект к указанному типу."; } else if (error instanceof ArrayIndexOutOfBoundsException || error instanceof IllegalStateException) { errorCode = 4; message = "Ошибка диапазона или состояния: Индекс вне границ или недопустимое состояние."; } else { errorCode = 5; message = "Общая ошибка выполнения: " + (originalMessage != null ? originalMessage : "Нет сообщения об ошибке"); } return new ErrorInfo(errorCode, message, originalMessage, stackTrace); }
Kotlin:
data class ErrorInfo( val code: Int, val message: String, val originalMessage: String?, val stackTrace: String ) fun handleError(error: Throwable): ErrorInfo { var errorCode = 0 var message = "Неизвестная ошибка" when (error) { is IllegalArgumentException -> { errorCode = 1 message = "Ошибка аргумента: Передано недопустимое значение." } is NullPointerException -> { errorCode = 2 message = "Ошибка ссылки: Попытка обращения к null-объекту." } is ClassCastException -> { errorCode = 3 message = "Ошибка типа: Невозможно привести объект к указанному типу." } is ArrayIndexOutOfBoundsException, is IllegalStateException -> { errorCode = 4 message = "Ошибка диапазона или состояния: Индекс вне границ или недопустимое состояние." } else -> { errorCode = 5 message = "Общая ошибка выполнения: ${error.message}" } } return ErrorInfo( code = errorCode, message = message, originalMessage = error.message, stackTrace = error.stackTraceToString() ) }
JavaScript:
function handleError(error) { let errorCode = 0; let message = "Неизвестная ошибка"; if (!error || !(error instanceof Error)) { return { code: 0, message: "Передан некорректный объект ошибки" }; } if (error instanceof SyntaxError) { errorCode = 1; message = "Ошибка синтаксиса: Неверный формат кода или данных."; } else if (error instanceof ReferenceError) { errorCode = 2; message = "Ошибка ссылки: Обращение к несуществующей переменной."; } else if (error instanceof TypeError) { errorCode = 3; message = "Ошибка типа: Операция выполнена над значением неподходящего типа."; } else if (error instanceof RangeError) { errorCode = 4; message = "Ошибка диапазона: Значение выходит за допустимые пределы."; } else { errorCode = 5; message = `Общая ошибка выполнения: ${error.message}`; } return { code: errorCode, message: message, originalMessage: error.message, stack: error.stack }; }
Python:
import traceback class ErrorInfo: def __init__(self, code, message, original_message, stack_trace): self.code = code self.message = message self.original_message = original_message self.stack_trace = stack_trace def __str__(self): return f"ErrorInfo(code={self.code}, message='{self.message}', original='{self.original_message}')" def handle_error(error: Exception) -> ErrorInfo: error_code = 0 message = "Неизвестная ошибка" original_message = str(error) stack_trace = "".join(traceback.format_exception(type(error), error, error.__traceback__)) if isinstance(error, (ValueError, TypeError)): error_code = 1 message = "Ошибка аргумента: Передано недопустимое значение или тип." elif isinstance(error, (KeyError, IndexError)): error_code = 2 message = "Ошибка ссылки: Попытка доступа к несуществующему ключу или индексу." elif isinstance(error, FileNotFoundError): error_code = 3 message = "Ошибка ресурса: Файл или директория не найдены." elif isinstance(error, (AttributeError, NotImplementedError)): error_code = 4 message = "Ошибка атрибута или реализации: Объект не имеет нужного атрибута или метод не реализован." else: error_code = 5 message = f"Общая ошибка выполнения: {original_message}" return ErrorInfo(error_code, message, original_message, stack_trace)
Обзор библиотек журналирования
Logback |
Log4j2 |
Winston |
Pino |
logging |
structlog |
|
Язык программирования |
Java |
Java |
JavaScript |
JavaScript |
Python |
Python |
Наибольший уровень уязвимости по CVSS |
9.8 |
10 |
9.8 |
9.8 |
- |
- |
Безопасные версии |
1.5.19 |
- |
3.2.1 |
7.0.0 |
3.12 |
- |
Logback
Преемник Log4j, написан тем же автором (Ceki Gülcü). Настраивается через XML. Поддерживает автоматическую настройку без перезапуска приложения, имеет интеграцию с SLF4J (стандартный интерфейс логирования). Позволяет настраивать фильтры и маскирование чувствительных данных (например, паролей) прямо в конфигурации или через кастомные конвертеры. Выводит JSON (через logback‑json‑classic), что критично для передачи логов в ELK или Loki.
Уязвимости: CVE-2017-5929 (9.8) CVE-2023-6378 (7.1), CVE-2023-6481 (7.1), CVE-2019-14439 (7.5), CVE-2019-12384 (5.9), CVE-2024-12798 (5.9), CVE-2023-23591 (4.9).
Log4j2
Современная версия Log4j, исправившая многие архитектурные недостатки оригинала. Подходит для высоконагруженных систем. Из плюсов: асинхронность, скорость, поддержка плагинов для множества форматов и протоколов. Поддерживает около 10 основных форматов, в том числе JSON. После инцидента с уязвимостью Log4Shell (CVE-2021-44228) критически важно всегда использовать актуальные версии и отключать поиск JNDI, если он не нужен. А в 2026 году выявлены новые уязвимости.
Уязвимости: CVE-2021-44228 (10.0), CVE-2023-50780 (8.8), CVE-2026-34481 (7.5), CVE-2026-34480 (7.5), CVE-2021-44832 (6.6), CVE-2021-45105 (5.9), CVE-2026-34477 (5.9), CVE-2025-68161 (4.8).
Winston
Подходит для приложений, где важна гибкость конфигурации и разнообразие выходов, а не скорость. Из особенностей: модульность, синхронность некоторых операций, и большое количество проверок, много готовых транспортов (вывод в консоль, файлы, базы данных, облачные сервисы). Выводит JSON пользовательского формата.
Уязвимости: CVE-2020-16259 (9.8), CVE-2020-16263 (9.1), CVE-2020-16257 (9.8), CVE-2020-16256 (8.8), CVE-2020-16262 (7.8), CVE-2020-16260 (7.5), CVE-2020-16258 (7.1), CVE-2020-16261 (6.8), CVE-2026-8240 (5.3), CVE-2026-8204 (5.3), CVE-2014-2060 (5.0), CVE-2026-8236 (4.3), CVE-2026-8340 (4.3), CVE-2026-8347 (4.3), CVE-2026-1981 (4.3), CVE-2025-13793 (4.3).
(Более подробные описания обо всех уязвимостях можно посмотреть по ссылке https://nvd.nist.gov/vuln/detail/+ номер CVE).
Pino
Идеален для микросервисов с высокой нагрузкой, где каждый миллисекунд на счету. Его вывод сразу готов для парсинга агрегаторами. Из плюсов — производительность. Пишет логи в формате JSON (NDJSON (потоковый JSON), можно подключить пакет pino‑pretty) с минимальными накладными расходами. Использует буферизацию и асинхронную запись, что делает его одним из самых быстрых логгеров для Node.js. Существует уязвимость AIKIDO-2026-10046, однако, примера CVE для неё на момент написания не выявлено, а уязвимость исправлена в версии 10.1.1. Ещё есть CVE-2019-15605, но она относится больше к самому node.js, чем к конкретно этой библиотеке. Но его уровень опасности я всё равно указала в таблице.
Logging
Логгер в Python по умолчанию. Встроен в стандартную библиотеку, мощный. Важно не путать с модулем python‑json‑logger, который содержит CVE-2025-27607 (8.8) на основе CWE-829.
По умолчанию выводит текст, что неудобно для машинного анализа, что требует настройки JSONFormatter для структурированного вывода.
Structlog
Дополнительный структурированный Python‑логгер. Позволяет добавлять контекст (например, trace_id, user_id) ко всем логам автоматически, ведутся работы по совмещению с модулем logging. Помогает избежать CWE-117, так как данные передаются как отдельные поля словаря, а не конкатенируются в строку, что снижает риск инъекций управляющих символов в сам формат журнала.
Уязвимости для указанных Python‑библиотек журналирования я не обнаружила, но вы можете указать их в комментариях.
Очень важно использовать наиболее актуальные версии библиотек журналирования! Чем новее версия, тем меньше для неё нашли уязвимостей.
Выбор зависит от задач:
Если нужна максимальная скорость в Node.js — Pino.
Если важна гибкость в Node.js — Winston.
В Java — Logback для баланса, Log4j2 для высокой нагрузки.
В Python — logging и/или structlog для структурированных данных.
Итог
Сделайте так, чтобы ваши журналы достались вам и только вам. Шифруйте журналы, а потом их ротируйте. Даже зашифрованные записи могут быть расшифрованы, поэтому используйте trace_id и correlation_id. Не выводите объект ошибки целиком, особенно с файловыми путями, именами библиотек, персональными данными. Сделайте сообщения для пользователей, закодировав основные ошибки вроде «error 404» в понятный и известный вам словарь. Маскируйте чувствительную информацию в журналах, в которой может быть многое, чтобы враг не догадался, а вы могли. Используйте актуальное ПО, к которому ещё не подобрали «отмычки» в виде эксплойтов уязвимостей. Подбирайте способ обезопасить вашу программу, который подойдёт именно вам, ориентируясь на ресурсы, расположение программы и технический стек.
Язык |
Вызов ошибки неправильный |
Вызов ошибки правильный |
Вызов ошибки идеальный |
Java |
|
|
|
Kotlin |
|
|
|
Python |
|
|
|
JS/TS |
|
|
|
Комментарии (2)

4tunade
30.07.2026 13:55Думаю, что такие реальные кейсы были много где. Здесь даже описана одна из библиотек, по которой потом приходилось очень долго искать, в каких же сервисах она использовалась из-за плохо настроенных процессов.
seregapolygon
Статья поднимает тему утечки информации через логи. А сталкивались ли вы в своей практике с реальными кейсами, когда именно неправильно настроенное логирование стало отправной точкой для успешной атаки? Как быстро вы это обнаружили и как исправляли?