Привет, Хабр! Меня зовут Ноянова Наталья, около 10 лет я исследую и работаю в ИБ, сейчас занимаюсь статическим анализом кода, изучаю его недостатки и уязвимости. Один из типов таких недостатков кода привлек моё внимание. Анализаторы кода описывают это как внутренние или внешние утечки информации, которые связаны с журналированием ошибок работы программы. И правки этого недостатка кода навели меня на мысли, которые изложены в данной статье. Для исправления подобных недостатков требуется не просто внести правки в код (вроде замены ссылок, начинающихся с «http» на «https»), а произвести целый ряд действий: перенастроить конфигурацию работы программы, понять суть безопасного логирования, найти баланс между необходимым и излишним. Ниже примеры таких решений, а также причины, почему это стоит вашего внимания. Сперва — общая теория, затем — обзор практик безопасного логирования, что будет, если вы его не настроите. Спойлер — сперва поймут, чем вы пользуетесь, затем подберут уязвимости для него, настроят эксплойты и применят. Журналирование ошибок — это критически важная часть жизненного цикла программного обеспечения. Оно упрощает отладку, позволяет выявлять сбои на ранней стадии и предотвращать их повторение. Однако журналы (жарг. логи) должны быть не только информативными, но и безопасными.Статья написана на основе горького опыта наблюдения за ошибками разработчиков и участия в CTF. Приведена общая информация о журналировании ошибок, библиотеках для этого, как это может сыграть на руку хакерам и как от этого можно предотвратить. Читать далее
Привет, Хабр! Меня зовут Ноянова Наталья, около 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 от остального текста журнала.
"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) | Описание |
Неизвестная ошибка | 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 | - |
Преемник 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 |
|
|
|
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Подножка для AI в виде UTF-8 | 0 | 10.07 | 14-10-2025 |
| 2 | IllegalMonitorStateException. Красим код в поисках монитора | 0 | 12.6 | 29-07-2026 |
| 3 | Ааа, всё пропало! AI создаёт дырявый код! Что же делать? | 0 | 11.43 | 16-06-2026 |
| 4 | 10 самых интересных ошибок в Java проектах за 2025 год | 0 | 12.89 | 26-12-2025 |
| 5 | Ошибки в UX у Хабр | 0 | 7.84 | 17-07-2026 |
| 6 | Книга: «100 ошибок C++ и как их избежать» | 0 | 7.01 | 03-06-2026 |
| 7 | Что сломается в вашем сервисе завтра, если сегодня вы запустите контейнеры без проверки образов | 0 | 6.37 | 24-07-2026 |
| 8 | Мост между SAST и фаззингом: как из сработки SAST получить подтверждённую уязвимость | 0 | 7.35 | 22-07-2026 |
| 9 | Материалы по хакингу на русском | 5 | 7 | 07-07-2026 |
| 10 | Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов | 5 | 8 | 07-07-2026 |