Heap‑дампы JVM — бесценный источник информации при разборе аварий с OutOfMemory и оптимизации производительности. Но вместе с тем они же — потенциальные каналы утечки данных, ведь будучи снятыми с боевого сервиса, дампы уносят в себе всё, с чем работал сервис на момент снимка: логины, пароли (иногда в открытом виде), важные ID и тому подобное — словом, всяческие sensitive данные, которые не нужны для анализа, но которые навлекают на получателя дампа серьёзную ответственность и риски. Как этого избежать без ущерба делу — разбираемся под катом.


Для начала возьмём лабораторный, но реалистичный пример — Spring PetClinic REST — бэкендовую версию популярного демо‑приложения. Эта версия из коробки включает Spring Security, который, в свою очередь, при каждом запуске генерирует пароль для ограничения доступа к методам API. И хотя пароль там «игрушечный» (логируется в открытом виде на старте приложения), механизм его попадания в память и дальнейшего распространения вполне соответствует реальным кейсам у других конфиденциальных данных, поэтому для примера он нам подходит.

Прежде чем искать варианты решения проблемы, надо её увидеть. Благо, на выбранном примере для этого достаточно только:

  1. Склонировать репозиторий
    git clone https://github.com/spring-petclinic/spring-petclinic-rest.git

  2. Открыть его в OpenIDE или другой среде разработки

  3. Открыть там же терминал (или отдельно) и выполнить в нём:
    ./mvnw spring-boot:run.

Эта команда запустит PetClinic с дефолтными настройками (со встроенной БД).

Дальше снимаем дамп памяти и открываем его для анализа, например, с помощью Eclipse Memory Analyzer Tool (File -> Acquire Heap Dump…):

Фрагмент первого окна Eclipse MAT после открытия дампа
Фрагмент первого окна Eclipse MAT после открытия дампа

Среди этого полумиллиона объектов конфиденциальными данными может оказаться что угодно, поэтому ни в Eclipse MAT, ни в других популярных opensource‑инструментах нет волшебной кнопки «Покажи мне все возможные утечки». Зато в них (в частности, в MAT и VisualVM) есть возможность выполнять SQL‑подобные запросы к дампу, выуживая таким образом наиболее подозрительные объекты для проверки.

Основная идея этого подхода в том, что каждый класс представляется как таблица, каждое поле класса — как столбец таблицы, а каждый экземпляр — как её строка. Подробнее об этом можно узнать, например, из моего доклада на Joker 2024.

В данном случае из описания и исходников проекта мы знаем, что Spring Security используется в самом простом режиме, когда все данные для доступа хранятся прямо в памяти, а сами явки‑пароли представлены классом org.springframework.boot.autoconfigure.security.SecurityProperties.User:

public static class User {

  /**
  * Default user name.
  */
  private String name = "user";

  /**
  * Password for the default user name.
  */
  private String password = UUID.randomUUID().toString();

Этого достаточно, чтобы составить вот такой OQL‑запрос к дампу:

SELECT toString(user.name) AS User, toString(user.password) AS Password
FROM org.springframework.boot.security.autoconfigure.SecurityProperties$User user

Он вернёт логин и пароль, действовавшие на момент снятия дампа:

🎯 Вот от этих данных нам и нужно избавиться перед передачей дампа кому бы то ни было.

Аналогичным образом, опираясь на знания бизнес‑логики и внутреннего устройства приложения, можно составить несколько OQL‑запросов, точечно проверяющих наличие sensitive данных в дампе.

А дальше нужно понять, как и чем их можно вычистить.

Способ 1. Eclipse MAT.

Раз уж мы упомянули Memory Analyzing Tool, было бы логично прояснить его возможности по устранению конфиденциальных данных в дампах. Возможности такие есть, правда, расположены не в самом очевидном месте — нужно вызвать контекстное меню какого‑либо элемента дампа и выбрать Export Snapshot:

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

  • NONE — не меняет ничего;

  • NAMES — пытается обфусцировать только имена классов, полей и методов. Работает в связке с параметром map (см. ниже). Здесь ключевое слово «пытается», потому что этот подход сопряжен с кучей условностей и ограничений. Подробнее см. в справке на MAT.

  • BASIC — обнуляет все массивы char, int и byte, а также поля классов с типами char и byte. Это позволяет устранить большинство строковых паролей, а также приватных ключей, которые нередко представлены объектами BigInteger. Но этот режим оставляет нетронутыми поля всех остальных примитивных типов, а также их массивов, что может быть как полезно для анализа, так и опасно с т.з. утечек. Если же безопасность превыше всего, то есть вариант:

  • FULL — обнуляет вообще все поля и массивы, а заодно и false‑ифицирует все boolean'ы. Щадит только ссылки на объекты и размеры массивов, чтобы не ломать структуру дампа и сохранить его пригодность для анализа.

Второй релевантный нам параметр — map — указывает на properties‑файл, в который прописываются соответствия между оригинальными именами классов приложения и их обфусцированными версиями. Это может пригодиться, если вы собираетесь отдать дамп на анализ внешнему подрядчику и не хотите, чтобы он знал, чем занимается ваше приложение. Eclipse MAT генерирует этот файл сам, но если у вас есть лишний рабочий день и вы хотите наверняка сбить с толку вероятного противника, можете прописать эти сопоставления сами: для рассматриваемого примера их понадобится всего 77,5 тысяч. Столько набегает за счёт того, что в маппинг также попадают имена статических полей, а ещё сгенерированные и анонимные классы (включая лямбды). Выглядит это примерно так:

org.springframework.boot.web.server.WebServer = xod.kloblaiwaiwreen.quak.lur.thiarm.Seotreure
org.jspecify.annotations.NonNull = xod.sheopluy.pouprukliel.Trawuak
org.h2.engine.User@passwordHash = xod.gj.exiosy.Knud@tuhoucudruag
jdk.proxy3.$Proxy105@m9 = vuz.briarb.$Fliwoorm@EI
io.swagger.v3.core.util.PrimitiveType@NUMBER = ar.gioliob.is.wras.kind.Graclanaiflob@KREART
org.apache.logging.log4j.MarkerManager$$Lambda = xod.froark.pooniek.luand.Cequiayuaniats$Klourt

Чтобы не перестараться и не сломать парсинг самому себе, Eclipse MAT по умолчанию исключает из обфускации классы пакетов java.* и даёт исключить другие через параметр skip (регулярное выражение).

Наконец, чтобы провернуть обратную операцию, то есть восстановить дамп с исходными именами классов, можно воспользоваться параметром undo, а именно:

  1. Открыть обфусцированный дамп в MAT

  2. Снова выбрать пункт Export Snapshot

  3. Указать путь для сохранения восстановленного дампа

  4. Указать путь к properties‑файлу с маппингом (он будет только читаться)

  5. Поставить галочку undo (параметр redact должен остаться в NONE)

Таким образом можно не хранить у себя исходную копию дампа с оригинальными именами классов, а при необходимости воссоздать её из обфусцированной версии, используя файл маппинга в качестве своеобразного «ключа восстановления». Только важно помнить, что этот финт не годится для полей/массивов/примитивов внутри классов, так как если они были обфусцированы режимами redact=BASIC или FULL, то этот фарш назад уже не провернуть…

Здесь пора вспомнить, что наша основная задача — вычистить пароль Spring Security, а не засекретить все имена классов, поэтому проверим наличие пароля в дампе тем же OQL‑запросом после обфускации в режиме redact=BASIC. Вот что он вернёт:

Как видно, здесь теперь царит безопасность…

В целом, неплохо, но смущает, что для экспорта дампа нам потребовалось открыть его исходную версию и что‑то поделать с ней руками, а значит, мы могли увидеть в ней «лишние» данные, что уже влечёт за собой риски, ответственность и вот это всё. К счастью, Eclipse MAT поддерживает так называемый Batch Mode — возможность выполнять некоторые операции без пользовательского ввода, то есть через CLI. Входной точкой этого режима является скрипт ParseHeapDump.[sh|bat] в корневой директории MAT. Как подсказывает имя, изначально скрипт предназначен для автономного парсинга (больших) дампов, но его же можно использовать и для других задач, указывая их имена в качестве аргументов. Для экспорта дампа с обфускацией это может выглядеть примерно так:

./ParseHeapDump.sh plain.hprof -output=redacted.hprof -redact=BASIC org.eclipse.mat.hprof:export

Здесь же можно указывать и другие опции из диалога Export Heap Dump…, например, файл маппинга имен классов -map=myheapdump2.map. А если и этого мало, то можно вызвать всё это программно через Memory Analyzer API и докрутить нужные поведения уже на уровне Java.

Скрытый текст

Для тех, у кого приведенные здесь манипуляции вызвали больше вопросов, чем понимания, или кто в принципе мало знаком с дампами и их анализом в Eclipse MAT, есть специальный тренинг, где можно не только разобраться с этой темой в теории, но и закрепить знания на практике с лабораторным приложением.

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

  • для работы с дампом Eclipse MAT должен его распарсить, а для этого ему нужно, в среднем, столько же оперативной памяти, сколько весит сам дамп;

    • как парсить большие дампы при малом объёме RAM можно почитать в этой заметке.

  • при парсинге MAT производит много вспомогательных индексов, размер которых может в разы превосходить размер исходного дампа, поэтому нужно много места на диске;

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

  • парсинг состоит из множества этапов, включающих многократные обходы исходного дампа, поэтому на больших объёмах это может занимать много времени;

    • у автора этих строк был период оптимизации одного сервера (64 GB RAM), когда можно было делать фиксы не чаще раза в день, потому что каждый новый дамп парсился по 4–5 часов (по ночам).

Да и сам Eclipse MAT — тот ещё комбайн: его дистрибутив весит под 100 МБ, поэтому если стоит задача включить его в состав CI/CD в качестве автоматического этапа обфускации дампов, полученных с боевого сервера, то этот инструмент — не лучший выбор.

К счастью, есть альтернативы.

Способ 2. Heap Dump Tool.

Компания PayPal, тесно работая с платёжными данными и имея часть приложений на JVM, вероятно, много раз сталкивалась с необходимостью анализа «опасных» heap‑дампов, поэтому разработала для этого специальный инструмент heap‑dump‑tool и выложила его в open source.

Это CLI‑инструмент под конкретную задачу — на вход подаёте исходный дамп, на выходе получаете «дезинфицированный»:

$ java -jar heap-dump-tool.jar sanitize leak.hprof sanitized.hprof
INFO  Application - heap-dump-tool (1.3.4 ca0325a, 2025-11-29T02:48:26.000-0800)
INFO  SanitizeCommandProcessor - Pre-processing ...
INFO  SanitizeCommandProcessor -     String fields to exclude from sanitization: java.lang.Thread#name,java.lang.ThreadGroup#name
INFO  SanitizeCommandProcessor -     Force match String.coder: true
INFO  SanitizeCommandProcessor - Input File: leak.hprof
INFO  SanitizeCommandProcessor - Starting heap dump sanitization ...
INFO  SanitizeCommandProcessor - Input File: leak.hprof
INFO  SanitizeCommandProcessor - Output File: sanitized.hprof
INFO  SanitizeCommandProcessor - Finished heap dump sanitization in 13s

Если на получившемся дампе выполнить тот же OQL‑запрос на выявление пароля, то он выдаст примерно следующее:

то есть сплошную безопасность.

Однако это не вся картина — на самом деле, по умолчанию Heap Dump Tool очищает только байтовые и символьные массивы, а целочисленные, вещественные и другие значения (в том числе массивы) сохраняет нетронутыми. Это может оставить лазейку для утечки, если, например, в каком‑то целочисленном поле хранился персональный ID клиента или сумма денег на его счету (кое‑где есть практика хранить денежные суммы в минимальных единицах валюты). Для таких случаев при вызове утилиты нужно добавить флажок --sanitize-byte-char-arrays-only=false (или кратко -s=false), и тогда вообще все примитивы и их массивы пойдут «под нож».

Такой режим не включен по умолчанию, вероятно, потому, что может усложнить дальнейший анализ. Разберём на примере. Допустим, нам нужно сопоставить данные дампа с метриками ОС или иной внешней телеметрией. Одним из главных «мостиков» для этого послужит PID процесса. И хотя его можно получить разными способами, удобнее всего было бы взять его напрямую из дампа — в случае Spring Boot для этого достаточно выполнить вот такой OQL‑запрос:

SELECT OBJECTS s.source
FROM org.springframework.boot.ApplicationInfoPropertySource s

Для исходного («сырого») дампа он вернёт HashMap примерно с таким содержимым:

Однако после обфускации этот результат, очевидно, изменится. И если spring.application.version нам не спасти в любом случае (так как это строковое значение), то целостность spring.application.pid целиком зависит от флага --sanitize-byte-char-arrays-only:

  • при значении true (по умолчанию) PID уцелеет

  • а при false содержимое мапы будет примерно таким:

то есть предельно безопасным.

Если бы PID был записан в отдельное поле какого‑либо класса, его ещё можно было бы спасти опцией --exclude-string-fields (кратко -e), которая говорит обфускатору, какие поля каких классов нужно оставить нетронутыми. По умолчанию такими полями являются только имена потоков и их групп (см. фрагмент лога выше).

У особо занудного внимательного читателя может возникнуть вопрос: «Зачем указывать команду sanitize, если инструмент и так создан для этого?» Дело в том, что он умеет не только обрабатывать готовые дампы, но и снимать их, причём снимать с приложений в контейнерах (в том числе когда сам запущен в контейнере). Для этого нужно сменить команду sanitize на capture, а путь к дампу — на имя целевого контейнера. В этом случае снятый дамп будет лежать внутри целевого контейнера и его нужно будет забрать оттуда руками (если успеете, пока его не прибил какой‑нибудь Kubernetes).

Даже не вникая в остальные возможности этого инструмента, уже должно стать ясно, что он хорошо подходит для нашей задачи:

  • вычищает пароли из дампа даже с умолчательными настройками

  • позволяет влиять на степень «дезинфекции»

  • не требует парсинга дампа

  • поставляется одним JAR‑файлом (сделан на Spring Boot + PicoCLI)

  • хорошо встраивается в CI/CD за счёт CLI‑интерфейса.

Но куда ж без минусов:

  • На огромных дампах инструмент может потребовать приседаний: под капотом он использует обычный BufferedInputStream с размером буфера 100 Мб. И если на небольших дампах этого вполне хватает, то по мере роста их размера можно оказаться перед неприятным выбором: либо дольше ждать при их чтении/записи, либо выделять сильно больше памяти под буфер.

  • В особо упоротых специфичных случаях может потребоваться кастомная логика обфускации, но heap‑dump‑tool не предоставляет возможностей по её модификации, а форкать его ради этого — затея сомнительная.

Едва ли много кто сталкивается с этими проблемами, но если вдруг, то вот вариант и на этот случай:

Способ 3. Hprof‑redact

Широко известный в узких кругах JVM‑спец Johannes Bechberger является автором множества полезных утилит для анализа и оптимизации производительности. И недавно с подачи другого известного в Java мире эксперта Volker«а Simonis»а он выпустил hprof‑redact — инструмент для обфускации дампов памяти перед передачей на анализ.

Инструмент во многом похож на описанный выше heap‑dump‑tool:

  • тоже CLI

  • тоже поставляется одним JAR‑архивом

  • тоже предлагает разные режимы редактирования данных.

Но есть и значимые отличия:

  • по заявлению автора, это пока только прототип (proof‑of‑concept), а не полноценный инструмент;

  • читает дампы в поточном режиме:

    • из плюсов: не требует тюнинга буфера для больших дампов

    • из минусов: делает два полных прохода по дампу, поэтому работает всё равно не мгновенно;

  • управляет редактированием через так называемый трансформеры, коих из коробки в версии 0.2.1 (февраль 2026) было три:

    • zero (умолчательный) — обнуляет все примитивы и заменяет содержимое строк нулями;

    • zero‑strings — тоже заменяет содержимое строк нулями, но оставляет примитивы нетронутыми;

    • drop‑strings — делает все строки пустыми (нулевой длины), но оставляет примитивы нетронутыми.

Наличие этих трансформеров, к слову, является главным дизайнерским отличием hprof‑redact от вышеупомянутого heap‑dump‑tool, поскольку трансформер — это часть публичного API инструмента, что позволяет при необходимости написать собственную логику обфускации, имплементировав развесистый, но полностью опциональный интерфейс HprofTransformer.

Отдельно для hrpof‑redact стоит упомянуть сжатие дампа при обработке. Оно есть и в других инструментах, но Eclipse MAT и heap‑dump‑tool реализуют его обычным GZip‑ом, а у hprof‑redact иной подход — он опустошает строки и примитивные массивы, сохраняя при этом информацию об их исходной длине. В этом смысле такое сжатие не совсем честно, ведь оно теряет данные без возможности восстановления. Однако и цель у него иная — оно призвано сократить объём дампа, сохранив его структурную, но не содержательную идентичность.

В отличие от heap‑dump‑tool, утилита hprof‑redact по умолчанию вычищает не только строки, но и примитивы (включая их массивы), поэтому оба приведенных выше OQL‑запроса (для выявления пароля и получения PID+версии приложения) вернут большое… ничего. Всё на радость отделу ИБ.

В целом, hprof‑redact пока сложно считать полноценным решением, поскольку он создан совсем недавно и пока что предоставляет не особо много возможностей. Однако он может стать достойной альтернативной тому же heap‑dump‑tool в случаях, когда требуется особая логика обфускации данных в дампе и вы готовы прописать её программно.

Способ 4. JDK (нет)

Прежде чем рассматривать какие‑то сторонние инструменты, было бы логичнее поинтересоваться, а нет ли чего‑то готового в стандартной библиотеке? Ответ: сейчас нет, но обсуждения ведутся.

В JIRA проекта OpenJDK есть тикет JDK-8337517, в котором предлагается доработать утилиту jcmd, а также добавить новую JVM‑опцию -XX:+HeapDumpRedacted, чтобы и та, и другая на выходе давали дезинфицированный дамп с занулёнными примитивами. Под этот тикет также есть соответствующий Pull Request на Github, однако он в статусе Closed, а сам тикет хоть и имеет статус Open, но поле Fix Version(s) у него имеет значение tbd, то есть «когда‑нибудь».

Основными причинами такого расклада стали невысокий приоритет задачи и отсутствие (пока) у разработчиков и ревьюеров единого взгляда на то, как должна быть имплементирована эта фича: что именно нужно очищать, какими должны быть аргументы вызова и как быть с тем, что если JVM выдаст обфусцированный дамп, то взять оригинальный будет уже негде. В этом обсуждении участвует и небезызвестный нам Алексей Шипилёв, комментируя в свойственной себе манере:

… while most of the time the confidential data is in primitive arrays (key material, cipher buffers, string contents), primitive fields carry identifiable data as well, e.g. numeric account/transaction IDs. Even double/floats contain data often, think financial data or even (pants heavily) LLM weights.

hprof‑redact является попыткой имплементировать эту фичу по исходной (авторской) задумке. Насколько (не)удачной она получилась — судить, в том числе, нам с вами, её пользователям, поэтому если у вас есть опыт её использования и соображения на его счёт, не стесняйтесь делиться ими, например, на странице разработчика или через GitHub issues.

Заключение

Для дезинфекции дампов перед их анализом существует не так уж много бесплатных инструментов. А основных среди них и вовсе только два:

  • Eclipse MAT — мощный, но тяжелый; требует многих приседаний, но предоставляет максимальную свободу действий (GUI / CLI / API);

  • heap‑dump‑tool — не богатый, зато лёгкий; отлично встраивается в pipeline«ы; умеет не всё, но многое (CLI / API).»

Особнячком от них стоит совсем новый и (пока) не глубоко проработанный инструмент hprof‑redact, во многом похожий на heap‑dump‑tool, но делающий ставку на гибкость Java API и поддержку больших дампов.

Чуть более формальное сравнение этих инструментов, во многом основанное на субъективных оценках автора, приведено в этой таблице:

Фича\инструмент

Eclipse MAT

Heap Dump Tool

Hprof‑redact

Работа с большими дампами

👍

👍👍

👍👍👍

Поддержка Docker контейнеров

Гибкость настроек обфускации

👍👍👍

👍

👍👍

Сжатие при обфускации

✅ (zip)

✅ (zip)

✅ (hprof)

Гибкость API

👍👍👍

👍

👍👍

Загрузка дампов в формате TAR

Пригодность для CI/CD

👍

👍👍

👍👍

Если вам доводилось решать такую задачу с помощью этих или иных инструментов, расскажите, пожалуйста, об этом в комментариях — ваш опыт станет отличным дополнением к этой статье.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.