Webpack собирает весь клиентский код в набор JavaScript-бандлов, которые загружаются браузером пользователя. Любой код, попавший в итоговый bundle.js, становится доступен:
Минификация и обфускация не являются механизмами защиты. Даже сильно сжатый production-бандл остаётся полностью доступным клиенту.
Главное правило безопасности фронтенда:
Всё, что попало в клиентский JavaScript, считается публичным.
Webpack активно работает с:
Во многих проектах разработчики ошибочно считают, что
.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.
Наиболее распространённая ошибка.
Нельзя хранить:
Плохо:
const STRIPE_SECRET = process.env.STRIPE_SECRET_KEY;
Если значение попадает в frontend build — ключ скомпрометирован.
Особенно опасны:
Никогда нельзя выносить:
JWT_SECRET=my_super_secret
JWT secret используется сервером для подписи токенов.
Если секрет окажется в браузере:
Опасные примеры:
AES_SECRET=secret
RSA_PRIVATE_KEY=...
Нельзя хранить:
Клиент не должен иметь доступ к данным, которые обеспечивают доверенную криптографию.
Критическая ошибка:
DB_PASSWORD=root
DB_USER=admin
Иногда такие значения случайно попадают в frontend через:
Последствия:
Нельзя раскрывать:
const INTERNAL_API = 'http://internal-admin:8080';
или:
const K8S_URL = 'http://kubernetes.default.svc';
Даже если пользователь не может напрямую обратиться к адресу, утечка инфраструктурных данных:
Плохо:
const ADMIN_API = '/api/admin/delete-user';
Даже если backend защищён авторизацией, не стоит:
Особенно опасно сочетание:
Часто утечки происходят через SDK.
Опасные примеры:
const SENTRY_AUTH_TOKEN = '...';
const NPM_TOKEN = '...';
const GITHUB_TOKEN = '...';
Некоторые сервисы имеют два типа ключей:
Например:
| Сервис | Public | Secret |
|---|---|---|
| Stripe | publishable key | secret key |
| Firebase | config public | admin secret |
| Sentry | DSN public | auth token |
| Mapbox | public token | secret token |
Разрешён только публичный вариант.
Некоторые ключи изначально предназначены для frontend.
Например:
const GOOGLE_MAPS_KEY = 'public_key';
или:
const STRIPE_PUBLISHABLE_KEY = 'pk_live_...';
Такие ключи:
Но даже public keys желательно ограничивать:
Безопасно:
const ENABLE_NEW_UI = true;
или:
const IS_BETA = false;
Такие данные не являются секретами.
Допустимо:
const API_URL = 'https://api.example.com';
Если endpoint публичный — скрывать его бессмысленно.
.envМногие ошибочно считают:
SECRET_KEY=123
раз .env не в Git — значит безопасно.
Но Webpack работает иначе:
.env;DefinePlugin — это текстовая замена на этапе сборки.
Пример:
new webpack.DefinePlugin({
__API_KEY__: JSON.stringify(process.env.API_KEY)
})
Webpack буквально заменяет:
console.log(__API_KEY__);
на:
console.log("secret");
Опасное заблуждение:
new Dotenv()
Плагин не скрывает значения.
Он лишь автоматически пробрасывает их в frontend.
Правильная схема:
Browser -> Backend -> External API
Неправильная:
Browser -> External API with secret
Правильно:
// frontend
fetch('/api/weather')
// backend
const response = await weatherApi.get({
key: process.env.WEATHER_SECRET
});
Секрет остаётся на сервере.
Хорошая практика:
PUBLIC_API_URL=https://api.site.com
PRIVATE_DB_PASSWORD=secret
Во frontend разрешаются только public-переменные.
Во многих системах используется специальный префикс.
Например:
PUBLIC_ANALYTICS_ID=xxx
или:
NEXT_PUBLIC_API_URL=...
VITE_API_URL=...
Смысл:
Даже если bundle минифицирован:
(()=>{var a="secret"})()
sourcemap может восстановить:
const secretApiKey = "super_secret";
Опасная конфигурация:
devtool: 'source-map'
Если .map файлы доступны публично:
Для production:
devtool: false
или:
devtool: 'hidden-source-map'
hidden-source-map позволяет:
Ошибка:
{
"dbPassword": "secret"
}
import config from './config.json';
Webpack включит JSON внутрь bundle.
Опасная структура:
shared/
config.js
module.exports = {
apiUrl: '...',
jwtSecret: '...',
dbPassword: '...'
};
Frontend импортирует:
import config from '../shared/config';
Весь объект попадает в клиентский код.
Опасный код:
window.__INITIAL_STATE__ = state;
Если в state присутствуют:
они окажутся в HTML страницы.
Ошибка:
return {
props: {
secret: process.env.DB_PASSWORD
}
};
Все props отправляются браузеру.
После production build необходимо анализировать:
Полезные проверки:
grep -Ri "secret" dist
grep -Ri "token" dist
grep -Ri "password" dist
Инструмент помогает увидеть:
Пример:
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
Опасно:
import db from './server/database';
Даже если код не используется напрямую, Webpack может включить часть зависимостей в граф модулей.
Безопасная структура:
src/
client/
server/
shared/
Frontend не должен импортировать:
Ошибка мышления:
unused code будет удалён
Не всегда.
Причины:
Даже неиспользуемый secret может случайно попасть в bundle.
Опасный пример:
if (adminMode) {
import('./adminSecrets');
}
Webpack всё равно создаст chunk.
Пользователь сможет:
Правильная схема:
.env
.env.client
.env.server
или:
config/
client.js
server.js
Полезная практика — автоматическая проверка бандла.
Например:
grep -Ri "AKIA" dist
Поиск AWS-ключей.
Content Security Policy:
Но CSP не скрывает:
Даже сильная обфускация:
var _0x12ab = ['secret'];
не скрывает данные.
Злоумышленник может:
Браузер полностью контролируется пользователем.
Следовательно:
Frontend должен получать:
Webpack:
Любая безопасность должна обеспечиваться архитектурой приложения, а не сборщиком.