VITE_ Is a Promotion Operator: How Build Pipelines Ship Server-Side Secrets to the Browser
A developer needs a backend API key to work in the frontend. The fastest fix: prefix the variable with VITE_. The build compiles. The CI passes. The AWS credential is now in 47 CDN edge nodes.
VITE_ and REACT_APP_ are not naming conventions. They are build-time promotion operators: they compile the environment variable's value as a literal string into the public JavaScript bundle. A single prefix mistake compiles a server-side secret into a public constant that ships in every cached JavaScript bundle. The documented blast radius runs from S3 access to full CI/CD pipeline compromise.
VITE_ and REACT_APP_ Are Build-Time Promotion Operators, Not Naming Conventions
Vite replaces import.meta.env.VITE_STRIPE_SECRET_KEY with the literal value at build time. There is no runtime lookup, no indirection, no protection layer. The final bundle contains the credential as a plain string in clear text:
// Source code: never reaches the browser in this form
const client = new Stripe(import.meta.env.VITE_STRIPE_SECRET_KEY)
// What the build compiles into the public bundle
const client = new Stripe("sk_live_51Habcdefghijklmnopqrstuvwxyz")
Create React App does the same with process.env.REACT_APP_* in the same compilation pass. Webpack and Parcel follow the same model with their respective directives. The framework makes no distinction between a publishable key designed for the browser and an IAM credential that authorizes infrastructure changes. Both become public constants if prefixed.
Sprocket Security documented a case where a live VITE_AWS_ACCESS_KEY, validated via sts:GetCallerIdentity, was present in a production bundle. From that key, the attacker accessed S3 with the full codebase. They then obtained CircleCI environment variables, a GitHub token, and full pipeline control. An analysis of 1 million domains found over 18,000 exposed API secrets. OpenAI key leaks grew 1,212% compared to 2022.
Source Maps Ship the Commented Original: Variable Names, String Literals, and All
The //# sourceMappingURL=bundle.js.map comment in the production bundle causes browsers to automatically request the .map file when DevTools is opened. The .map file contains the original source with descriptive variable names, inline comments, and the exact string literals that minification tried to obscure. Everything the minifier hid is preserved in plain text in the map file.
CVE-2024-27257 documented exactly this vector in IBM OpenPages 8.3 and 9.0. Publicly served source maps exposed client-side source code to unauthorized users. CVSS 4.3, patched September 2024, classified as CWE-540.
The mitigation for nginx servers is one line: location ~* \.map$ { deny all; }. Source maps should be generated during the build and uploaded only to error tracking services such as Sentry or Datadog. Never serve source maps publicly. The check belongs in the deploy pipeline, not in a best-practices document.
HackerOne's Paper Trail: Four Platforms, the Same Grep
Bug bounty hunters run 3 grep commands against a site's JavaScript assets and routinely find API keys with account-level or organization-level access. HackerOne has disclosed reports documenting this pattern against payment, analytics, and SaaS platforms. The same search a bug bounty hunter runs is the same one any attacker runs.
HackerOne #508024 (Omise): both the public key and the secret payment API key were exposed in JavaScript. The secret key enabled creating customers, cards, and charges on the platform. The confusion between publishable key and secret key is the most common pattern in this category.
HackerOne #2307933 (Datadog via Mars): Datadog API key and application key embedded in a client-side JavaScript file. Both keys were validated as active at time of disclosure. HackerOne #1066410 (Clario): Google API key found in JavaScript files on the account page. HackerOne #1218754 (Semrush): api.semrush.com API key leaked in client-side JavaScript.
All 4 were found with grep -E "(secret|api_key|apikey|token)" bundle.js. That is the same command any attacker runs in 30 seconds:
grep -rE "AKIA[0-9A-Z]{16}" dist/ # AWS Access Key
grep -rE "sk_live_[a-zA-Z0-9]+" dist/ # Stripe secret key
grep -rE "ghp_[a-zA-Z0-9]+" dist/ # GitHub personal token
Mobile Apps: Rotating the APK Requires an App Store Release
React Native compiles JavaScript into the APK or IPA at build time. A secret in the bundle cannot be rotated by redeploying a web server. It requires submitting a new binary to the App Store and waiting for user adoption. The exposure window is measured in weeks, not minutes.
CVE-2024-21668 (react-native-mmkv < 2.11.0) demonstrates an adjacent vector: the MMKV encryption key was logged to the Android system log. It was recoverable via adb logcat. CVSS 4.9, CWE-532, disclosed January 2024. Only Android was affected; iOS was not.
Frida extracts strings from a running React Native app's JavaScript engine at runtime without requiring full decompilation. The APK can be decompiled with apktool + dex2jar, exposing the bundled JavaScript including environment variable values in plain text. The App Store review cycle takes 24 to 72 hours for iOS. The old binary with the exposed key stays installed on user devices throughout the entire adoption lag.
Third-Party Scripts Run in Your Context and Read Every Global You Set
Scripts with access to window-scoped globals can read credentials that VITE_ promotion placed there, making third-party script control a second line of defense. Any <script src="..."> from a third-party domain runs with full access to window and all JavaScript variables in scope. A supply chain compromise of a single vendor is a credential exfiltration vector against every site loading that script.
The Polymarket supply chain attack in 2026: a compromised third-party vendor injected malicious JavaScript into the platform's frontend. Roughly $3M was stolen from fewer than 15 wallets via token approval phishing. The payload was conditional, served selectively to targeted users.
Magecart compromised 11,000 e-commerce sites in 2024 via third-party JavaScript injection into checkout page contexts. That number was a threefold increase from 2023. CSP controls which scripts load, but cannot prevent a trusted script from reading window-scoped variables and sending them to an attacker-controlled endpoint. A script already on the allowlist can exfiltrate data to any endpoint its author controls.
The Fix Is Architectural: Secrets Belong Behind a Server Process
The correct defense is not a naming convention audit. It is eliminating the reason for server-side secrets to be in the build pipeline at all.
The server proxy pattern keeps the secret in the server context. The frontend calls its own backend route. The backend calls the third-party API with the secret key. The browser never receives the credential. Next.js route handlers, Remix loaders, and Bun/Elysia endpoints all work this way.
The CI scanning gap: Gitleaks and TruffleHog are typically run against source repositories. Adding a CI step that scans ./dist/*.js before the CDN upload catches promotion mistakes that source-only scanning misses:
gitleaks detect --source=./dist --no-git
trufflehog filesystem ./dist --only-verified
GitHub push protection flags high-entropy strings in diffs. The MAGO Intel tool (intel.mago.team) scans JavaScript bundles for exposed key patterns, covering Stripe, AWS, Google, and Datadog credential formats.
The build pipeline has no concept of secret classification. It inlines every VITE_-prefixed variable identically, regardless of whether the value is a publishable key or an IAM credential. That judgment belongs to the developer and to the CI gate before the artifact reaches a CDN.
Top comments (0)