Исключение чувствительных данных из бандла

В процессе сборки JavaScript-приложений Rollup формирует единый или набор оптимизированных файлов на основе графа зависимостей. На этом этапе нередко происходит неочевидное включение данных, которые не должны попадать в клиентскую часть: API-ключей, токенов доступа, внутренних URL, конфигураций окружения и диагностической информации.

Источником таких утечек становятся:

  • прямое объявление секретов в исходном коде;
  • импорт конфигурационных модулей с серверными параметрами;
  • использование .env файлов без фильтрации;
  • подстановка значений через плагины без контроля экспорта;
  • подключение сторонних библиотек, содержащих встроенные ключи или debug-логи.

Rollup, в отличие от серверных рантаймов, не различает «секретные» и «несекретные» данные. Любой импортируемый идентификатор, попавший в граф зависимостей и не удалённый tree-shaking-ом, может оказаться в итоговом бандле.


Статическая подстановка значений и её риски

Одним из ключевых механизмов Rollup является статическая трансформация кода. При этом выражения вида:

const API_URL = process.env.API_URL;

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

Особенно опасны конструкции:

const TOKEN = "secret_token_123";
const KEY = config.privateKey;

При отсутствии дополнительной обработки такие значения становятся частью клиентского JavaScript и могут быть извлечены из браузера любым пользователем.


Использование @rollup/plugin-replace

Одним из основных инструментов контроля подстановки значений является плагин замены строк.

import replace from '@rollup/plugin-replace';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'es'
  },
  plugins: [
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify('production'),
      'process.env.API_URL': JSON.stringify('https://api.example.com')
    })
  ]
};

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


Изоляция конфигурации окружения

Безопасная работа с окружением предполагает разделение конфигурации на публичную и приватную части.

Типовая структура:

config/
  public.js
  private.js

public.js:

export const config = {
  apiBase: 'https://api.example.com',
  appVersion: '1.0.0'
};

private.js:

export const secrets = {
  apiKey: process.env.API_KEY,
  dbPassword: process.env.DB_PASSWORD
};

В Rollup сборке допускается импорт только публичного модуля:

import { config } from './config/public.js';

При этом приватный модуль исключается через external или условную компиляцию.


Механизм external для предотвращения включения модулей

Поле external в конфигурации Rollup позволяет исключить зависимости из бандла.

export default {
  input: 'src/index.js',
  external: ['dotenv', './config/private.js']
};

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


Условная компиляция и dead code elimination

Tree-shaking в Rollup удаляет недостижимый код, но его эффективность зависит от статической природы условий.

Пример:

if (process.env.SHOW_DEBUG === 'true') {
  console.log('Debug mode');
}

При корректной подстановке через replace:

replace({
  'process.env.SHOW_DEBUG': JSON.stringify('false')
});

код трансформируется в:

if ('false' === 'true') {
  console.log('Debug mode');
}

и впоследствии может быть удалён как dead code.

Однако если значение не является статическим, например:

const flag = getRuntimeFlag();
if (flag) {
  console.log('Debug');
}

Rollup не способен безопасно удалить такой блок.


Обработка .env файлов

Для работы с переменными окружения часто используется загрузка .env через Node.js слой:

import dotenv from 'dotenv';
dotenv.config();

Однако прямое подключение dotenv в клиентский бандл приводит к утечке всей структуры process.env.

Корректный подход — извлечение значений на этапе сборки:

import { config } from 'dotenv';
import replace from '@rollup/plugin-replace';

config();

export default {
  plugins: [
    replace({
      preventAssignment: true,
      'process.env.API_KEY': JSON.stringify(process.env.API_KEY)
    })
  ]
};

После этого dotenv исключается через external, чтобы не попасть в итоговый bundle.


Ошибки архитектуры, приводящие к утечкам

Типовые причины попадания секретов в бандл:

  1. Централизованный конфиг с смешанными данными
export const config = {
  apiUrl: '...',
  apiKey: process.env.API_KEY
};
  1. Экспорт секретов из утилитных модулей
export const getKey = () => process.env.API_KEY;
  1. Логирование окружения
console.log(process.env);
  1. Использование клиентских SDK с встроенными ключами без проверки источника

Rollup не различает критичность данных — любая экспортируемая сущность считается частью публичного API модуля.


Защита через разделение сборок

Практика разделения на client/server сборки снижает вероятность утечек.

Пример конфигурации:

export default [
  {
    input: 'src/client.js',
    output: {
      file: 'dist/client.js',
      format: 'es'
    }
  },
  {
    input: 'src/server.js',
    output: {
      file: 'dist/server.js',
      format: 'cjs'
    }
  }
];

Серверная сборка содержит доступ к секретам, клиентская — только публичные данные.


Инлайнинг переменных и контроль точек внедрения

Неконтролируемый инлайнинг может привести к попаданию чувствительных данных в неожиданные места.

Особенно критичны:

  • шаблонные строки;
  • конфигурационные фабрики;
  • функции с замыканиями над секретами.

Пример проблемного кода:

const secret = process.env.API_KEY;

export const requestUrl = (path) =>
  `${secret}@api.service.com/${path}`;

После сборки значение secret становится частью строки URL, доступной в клиенте.


Минимизация поверхности экспорта

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

  • публичные URL;
  • feature flags без чувствительных данных;
  • версии и метаданные.

Любой экспортируемый объект рассматривается как потенциально публичный:

export const publicConfig = {
  apiUrl: 'https://api.example.com'
};

Контроль сторонних зависимостей

Часто утечки происходят не в собственном коде, а через зависимости. Некоторые пакеты могут содержать:

  • debug-конфигурации;
  • встроенные ключи API;
  • тестовые endpoints;
  • неочищенные console-выводы.

Rollup позволяет анализировать граф зависимостей через плагины визуализации, однако финальная ответственность за включение пакетов остаётся на уровне конфигурации сборки.


Плагины для усиления контроля

Помимо replace, применяются дополнительные механизмы:

  • плагин анализа bundle (visualizer);
  • плагины удаления console и debugger;
  • кастомные трансформеры AST;
  • фильтрация env-переменных.

Пример удаления логов:

terser({
  compress: {
    drop_console: true,
    drop_debugger: true
  }
});

Принцип «нулевого доверия к бандлу»

Любой код, попадающий в клиентскую сборку, считается публичным независимо от его источника. Это фундаментальное ограничение архитектуры frontend-сборки.

Rollup обеспечивает инструменты трансформации и оптимизации, но не обеспечивает классификацию данных. Контроль чувствительности реализуется исключительно на уровне структуры проекта и конфигурации сборки.