Безопасность: что нельзя выносить в клиентский бандл

Webpack собирает весь клиентский код в набор JavaScript-бандлов, которые загружаются браузером пользователя. Любой код, попавший в итоговый bundle.js, становится доступен:

  • через DevTools браузера;
  • через sourcemap-файлы;
  • через декомпиляцию минифицированного кода;
  • через сетевые запросы;
  • через кэш браузера;
  • через анализ загруженных модулей.

Минификация и обфускация не являются механизмами защиты. Даже сильно сжатый production-бандл остаётся полностью доступным клиенту.

Главное правило безопасности фронтенда:

Всё, что попало в клиентский JavaScript, считается публичным.


Почему Webpack часто становится причиной утечек

Webpack активно работает с:

  • переменными окружения;
  • DefinePlugin;
  • dotenv;
  • импортами конфигов;
  • JSON-файлами;
  • build-time константами.

Во многих проектах разработчики ошибочно считают, что .env автоматически защищён. На практике Webpack не хранит секреты — он встраивает их в код.

Например:

API_SECRET=super_secret_key
new webpack.DefinePlugin({
    'process.env.API_SECRET': JSON.stringify(process.env.API_SECRET)
})

После сборки:

const secret = "super_secret_key";

Секрет буквально вшивается в bundle.


Какие данные категорически нельзя помещать в клиентский бандл

API Secret Keys

Наиболее распространённая ошибка.

Нельзя хранить:

  • private API keys;
  • secret tokens;
  • access credentials;
  • admin API keys;
  • signing secrets.

Плохо:

const STRIPE_SECRET = process.env.STRIPE_SECRET_KEY;

Если значение попадает в frontend build — ключ скомпрометирован.

Особенно опасны:

  • Stripe Secret Key;
  • AWS Secret Access Key;
  • Firebase Admin SDK credentials;
  • Telegram bot token;
  • GitHub Personal Access Token;
  • OpenAI secret key;
  • SMTP credentials.

JWT Secret

Никогда нельзя выносить:

JWT_SECRET=my_super_secret

JWT secret используется сервером для подписи токенов.

Если секрет окажется в браузере:

  • пользователь сможет генерировать валидные токены;
  • возможна подмена ролей;
  • возможна эскалация привилегий;
  • аутентификация полностью компрометируется.

Приватные ключи шифрования

Опасные примеры:

AES_SECRET=secret
RSA_PRIVATE_KEY=...

Нельзя хранить:

  • приватные RSA-ключи;
  • AES-ключи;
  • HMAC secrets;
  • encryption passwords;
  • signing certificates.

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


Доступы к базе данных

Критическая ошибка:

DB_PASSWORD=root
DB_USER=admin

Иногда такие значения случайно попадают в frontend через:

  • DefinePlugin;
  • dotenv-webpack;
  • общий config.js;
  • shared constants.

Последствия:

  • компрометация базы;
  • удаление данных;
  • утечка персональной информации;
  • полный захват инфраструктуры.

Приватные URL внутренней инфраструктуры

Нельзя раскрывать:

const INTERNAL_API = 'http://internal-admin:8080';

или:

const K8S_URL = 'http://kubernetes.default.svc';

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

  • помогает атакующему;
  • раскрывает архитектуру;
  • упрощает SSRF-атаки;
  • раскрывает внутренние сервисы.

Административные endpoint’ы

Плохо:

const ADMIN_API = '/api/admin/delete-user';

Даже если backend защищён авторизацией, не стоит:

  • раскрывать скрытые endpoint’ы;
  • раскрывать внутреннюю структуру API;
  • публиковать dev/admin routes.

Особенно опасно сочетание:

  • hidden endpoints;
  • слабой авторизации;
  • предсказуемых URL.

Секреты сторонних сервисов

Часто утечки происходят через SDK.

Опасные примеры:

const SENTRY_AUTH_TOKEN = '...';
const NPM_TOKEN = '...';
const GITHUB_TOKEN = '...';

Некоторые сервисы имеют два типа ключей:

  • public key;
  • secret key.

Например:

Сервис Public Secret
Stripe publishable key secret key
Firebase config public admin secret
Sentry DSN public auth token
Mapbox public token secret token

Разрешён только публичный вариант.


Что можно хранить в клиентском бандле

Public API Keys

Некоторые ключи изначально предназначены для frontend.

Например:

const GOOGLE_MAPS_KEY = 'public_key';

или:

const STRIPE_PUBLISHABLE_KEY = 'pk_live_...';

Такие ключи:

  • ограничиваются доменом;
  • не дают полного доступа;
  • проектируются как публичные.

Но даже public keys желательно ограничивать:

  • по origin;
  • по IP;
  • по quota;
  • по referer;
  • по allowed APIs.

Feature Flags

Безопасно:

const ENABLE_NEW_UI = true;

или:

const IS_BETA = false;

Такие данные не являются секретами.


Публичные URL

Допустимо:

const API_URL = 'https://api.example.com';

Если endpoint публичный — скрывать его бессмысленно.


Опасность переменных окружения в Webpack

Иллюзия безопасности .env

Многие ошибочно считают:

SECRET_KEY=123

раз .env не в Git — значит безопасно.

Но Webpack работает иначе:

  1. Node.js читает .env;
  2. значение попадает в process.env;
  3. DefinePlugin подставляет строку в код;
  4. секрет попадает в bundle.

DefinePlugin не защищает данные

DefinePlugin — это текстовая замена на этапе сборки.

Пример:

new webpack.DefinePlugin({
    __API_KEY__: JSON.stringify(process.env.API_KEY)
})

Webpack буквально заменяет:

console.log(__API_KEY__);

на:

console.log("secret");

dotenv-webpack тоже не защищает секреты

Опасное заблуждение:

new Dotenv()

Плагин не скрывает значения.

Он лишь автоматически пробрасывает их в frontend.


Безопасная архитектура

Секреты должны храниться только на сервере

Правильная схема:

Browser -> Backend -> External API

Неправильная:

Browser -> External API with secret

Backend Proxy Pattern

Правильно:

// frontend
fetch('/api/weather')
// backend
const response = await weatherApi.get({
    key: process.env.WEATHER_SECRET
});

Секрет остаётся на сервере.


Разделение env-переменных

Хорошая практика:

PUBLIC_API_URL=https://api.site.com
PRIVATE_DB_PASSWORD=secret

Во frontend разрешаются только public-переменные.


Префиксы публичных переменных

Во многих системах используется специальный префикс.

Например:

PUBLIC_ANALYTICS_ID=xxx

или:

NEXT_PUBLIC_API_URL=...
VITE_API_URL=...

Смысл:

  • всё без префикса считается серверным;
  • всё с префиксом считается публичным.

Утечки через sourcemaps

Почему sourcemaps опасны

Даже если bundle минифицирован:

(()=>{var a="secret"})()

sourcemap может восстановить:

const secretApiKey = "super_secret";

Production sourcemaps

Опасная конфигурация:

devtool: 'source-map'

Если .map файлы доступны публично:

  • раскрывается исходный код;
  • раскрываются названия переменных;
  • раскрывается архитектура;
  • упрощается reverse engineering.

Безопасные варианты

Для production:

devtool: false

или:

devtool: 'hidden-source-map'

hidden-source-map позволяет:

  • отправлять sourcemaps в Sentry;
  • не публиковать их пользователю.

Утечки через JSON и конфиги

Импорт config.json

Ошибка:

{
    "dbPassword": "secret"
}
import config from './config.json';

Webpack включит JSON внутрь bundle.


Shared config между backend и frontend

Опасная структура:

shared/
    config.js
module.exports = {
    apiUrl: '...',
    jwtSecret: '...',
    dbPassword: '...'
};

Frontend импортирует:

import config from '../shared/config';

Весь объект попадает в клиентский код.


Утечки через SSR и hydration

Ошибки при сериализации состояния

Опасный код:

window.__INITIAL_STATE__ = state;

Если в state присутствуют:

  • токены;
  • credentials;
  • server config;
  • internal flags;

они окажутся в HTML страницы.


Утечки через Next.js props

Ошибка:

return {
    props: {
        secret: process.env.DB_PASSWORD
    }
};

Все props отправляются браузеру.


Анализ итогового бандла

Проверка содержимого

После production build необходимо анализировать:

  • bundle.js;
  • chunks;
  • sourcemaps;
  • встроенные строки.

Полезные проверки:

grep -Ri "secret" dist
grep -Ri "token" dist
grep -Ri "password" dist

webpack-bundle-analyzer

Инструмент помогает увидеть:

  • какие модули попали в bundle;
  • какие зависимости утекли;
  • какие server-only библиотеки случайно импортированы.

Пример:

const BundleAnalyzerPlugin =
    require('webpack-bundle-analyzer')
        .BundleAnalyzerPlugin;

plugins: [
    new BundleAnalyzerPlugin()
]

Server-only модули

Ошибки импорта backend-кода

Опасно:

import db from './server/database';

Даже если код не используется напрямую, Webpack может включить часть зависимостей в граф модулей.


Разделение слоёв приложения

Безопасная структура:

src/
    client/
    server/
    shared/

Frontend не должен импортировать:

  • server;
  • api secrets;
  • db layer;
  • auth internals.

Tree Shaking не гарантирует безопасность

Ошибка мышления:

unused code будет удалён

Не всегда.

Причины:

  • side effects;
  • dynamic imports;
  • CommonJS;
  • неправильная конфигурация;
  • re-export chains.

Даже неиспользуемый secret может случайно попасть в bundle.


Dynamic import и секреты

Опасный пример:

if (adminMode) {
    import('./adminSecrets');
}

Webpack всё равно создаст chunk.

Пользователь сможет:

  • скачать chunk;
  • проанализировать содержимое;
  • извлечь секреты.

Environment separation

Разделение frontend и backend env

Правильная схема:

.env
.env.client
.env.server

или:

config/
    client.js
    server.js

Проверка на CI

Полезная практика — автоматическая проверка бандла.

Например:

grep -Ri "AKIA" dist

Поиск AWS-ключей.


CSP не защищает секреты

Content Security Policy:

  • защищает от XSS;
  • ограничивает загрузку скриптов;
  • ограничивает inline code.

Но CSP не скрывает:

  • bundle;
  • JS-код;
  • env values;
  • встроенные строки.

Обфускация не является защитой

Даже сильная обфускация:

var _0x12ab = ['secret'];

не скрывает данные.

Злоумышленник может:

  • выполнить код;
  • поставить breakpoint;
  • посмотреть runtime values;
  • извлечь строки.

Основные принципы безопасной сборки Webpack

Никогда не считать frontend доверенной средой

Браузер полностью контролируется пользователем.

Следовательно:

  • весь JS доступен;
  • весь HTML доступен;
  • все network requests доступны;
  • весь runtime доступен.

Всё секретное должно оставаться на backend

Frontend должен получать:

  • только публичные данные;
  • только разрешённые токены;
  • только ограниченные ключи.

Webpack — инструмент сборки, а не безопасности

Webpack:

  • не шифрует данные;
  • не скрывает env;
  • не защищает secrets;
  • не предотвращает reverse engineering.

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