Привет, Хабр! Меня зовут Ноянова Наталья, около 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‑запрос или один вызов метода).

Бизнес‑уровень (целая транзакция, сессия, заказ).

Отношение с запросами

Не меняется в пределах одного запроса.

Может охватывать множество запросов с разными trace_id.

Генерация

Генерируется на входе каждого нового запроса.

Часто передаётся клиентом или генерируется в начале бизнес‑процесса.

Цель

Отладка производительности и поиск ошибок в стеке вызовов.

Отслеживание бизнес‑логики и состояния транзакции.

Как они работают вместе?

В современных стандартах (например, W3C Trace Context) для технической трассировки часто используется именно trace_id. Однако в журналах и системах мониторинга (ELK, Splunk) очень удобно дублировать correlation_id (или использовать его как синоним trace_id в простых случаях), чтобы разработчики могли искать не только технические сбои, но и бизнес‑сценарии.

Пример сценария:

  1. Клиент отправляет запрос «Купить товар».

  2. Шлюз генерирует trace_id: A1 и correlation_id: Order-123.

  3. Сервис оплаты обращается к банку. У этого вызова новый trace_id: B1, но он наследует correlation_id: Order-123.

  4. Сервис склада обращается к базе данных. Новый 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, можно скрыть версии ПО:

  1. HTTP‑заголовки: удалите X-Powered-By иServer. В Nginx используйте server_tokens off;.

  2. HTML‑код: удалите комментарии с версиями и используйте минификацию.

  3. Пути к файлам: не используйте в URL версии (например, /js/react@18.2.0/). Используйте хеши содержимого.

  4. 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_iduser_id) ко всем логам автоматически, ведутся работы по совмещению с модулем logging. Помогает избежать CWE-117, так как данные передаются как отдельные поля словаря, а не конкатенируются в строку, что снижает риск инъекций управляющих символов в сам формат журнала.

Уязвимости для указанных Python‑библиотек журналирования я не обнаружила, но вы можете указать их в комментариях.

Очень важно использовать наиболее актуальные версии библиотек журналирования! Чем новее версия, тем меньше для неё нашли уязвимостей.

Выбор зависит от задач:

  • Если нужна максимальная скорость в Node.js — Pino.

  • Если важна гибкость в Node.js — Winston.

  • В Java — Logback для баланса, Log4j2 для высокой нагрузки.

  • В Python — logging и/или structlog для структурированных данных.

Итог

Сделайте так, чтобы ваши журналы достались вам и только вам. Шифруйте журналы, а потом их ротируйте. Даже зашифрованные записи могут быть расшифрованы, поэтому используйте trace_id и correlation_id. Не выводите объект ошибки целиком, особенно с файловыми путями, именами библиотек, персональными данными. Сделайте сообщения для пользователей, закодировав основные ошибки вроде «error 404» в понятный и известный вам словарь. Маскируйте чувствительную информацию в журналах, в которой может быть многое, чтобы враг не догадался, а вы могли. Используйте актуальное ПО, к которому ещё не подобрали «отмычки» в виде эксплойтов уязвимостей. Подбирайте способ обезопасить вашу программу, который подойдёт именно вам, ориентируясь на ресурсы, расположение программы и технический стек.

Язык

Вызов ошибки неправильный

Вызов ошибки правильный

Вызов ошибки идеальный

Java

try {
//code
}
catch (Exception e) {
System.out.println(e);
}

try {
//code
}
catch (Exception e) {
logger.error(“Неожиданная ошибка в методе code ‘, e.message());
}’”

try {
//code
}
catch (Exception e) {
if (e instanceof IllegalArgumentException) { e = 1;
message = “Invalid input”;
logger.error(message)}}

Kotlin

try { //code } catch (Exception e) { logger.error(“Произошла критическая ошибка: ${e.message}”, e)}

try { //code } catch (Exception e) { logger.error(“Произошла критическая ошибка: ${e.message}”)}

try {
//code
}
catch (Exception e) {
logger.error(errorInfo.code)

Python

try:
#code
except Exception as e:

print(e)
logging.error(e)
logging.error(traceback.format_exc())

try:
#code
except Exception as e:
logging.error(e)

try:
#code
except (Exception error):

info=handle_error(e)
log.logging(info)

JS/TS

try {
//code
} catch(e) {
alert(e.stack);
alert(e);

try {
//code
} catch(e) {
alert(e.stack);
alert(e);
try {
//code
} catch(e) {
alert(e.name);
alert(e.message);}

try {
//code
} catch(e) {

if (e instanceof SyntaxError) {
errorCode = 1;
message = “Ошибка синтаксиса: Неверный формат кода или данных.”;
}

Комментарии (2)


  1. seregapolygon
    30.07.2026 13:55

    Статья поднимает тему утечки информации через логи. А сталкивались ли вы в своей практике с реальными кейсами, когда именно неправильно настроенное логирование стало отправной точкой для успешной атаки? Как быстро вы это обнаружили и как исправляли?


  1. 4tunade
    30.07.2026 13:55

    Думаю, что такие реальные кейсы были много где. Здесь даже описана одна из библиотек, по которой потом приходилось очень долго искать, в каких же сервисах она использовалась из-за плохо настроенных процессов.