Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

Your CI artifacts and preview deployments can leak unprotected JavaScript

Many teams start obfuscating or VM-protecting client JavaScript in the production Vite or webpack build, then assume the problem is solved. The production CDN may be clean. Meanwhile, the unprotected bundle is sitting in

Many teams start obfuscating or VM-protecting client JavaScript in the production Vite or webpack build, then assume the problem is solved. The production CDN may be clean. Meanwhile, the unprotected bundle is sitting in a CI artifact, a preview URL, a GitHub Actions zip retained for 90 days, or a leftover object from last week's deploy.

This article is about the places unprotected JavaScript escapes after you "turned protection on," and how to close those holes without making pull-request previews useless.

Production is not the only place the bundle exists

A typical pipeline looks like this:

# conceptual
- run: npm run build
- uses: actions/upload-artifact
  with:
    name: dist
    path: dist/
- run: npm run deploy:preview   # often a cheaper, unprotected build
- run: npm run deploy:prod      # the one you finally protect

Three leaks fall out of that shape:

  1. The artifact is downloadable by anyone with repo access — and often by anyone who has a link, depending on visibility and retention. Artifacts on a public GitHub repository are a common surprise.
  2. Preview deployments usually skip the slow or paid protection step "to keep PR checks fast." The preview URL is still a public website with your real algorithms, just on a random subdomain.
  3. Failed or previous production deploys may remain reachable: an old CloudFront version, an un-overwritten /assets folder, a Docker image in a public or loosely permissioned registry.

If a competitor cannot get the protected production chunk, they will take the unprotected preview. The algorithm is the same.

Preview URLs are public more often than you think

Vercel preview URLs, Netlify deploy previews, Cloudflare Pages preview aliases, and similar PR instances are convenient. They are also, by default, reachable without your company VPN.

A protection policy that only runs when NODE_ENV=production and VERCEL_ENV=production (or CONTEXT=production on Netlify) leaves every branch deploy as a clean, pretty-printable copy of your client logic.

You do not have to protect previews the same way you protect production. You do have to decide, deliberately:

  • Lock previews behind authentication (Vercel Deployment Protection, a Netlify password, Cloudflare Access, or basic auth). This is the default you should want for anything that contains proprietary client logic.
  • Or run the same protection step on previews, accepting the extra build time.
  • Or ship a reduced preview: mock the proprietary function, use a stub solver, or omit the paid feature's client engine. A preview that cannot exercise the real algorithm cannot leak it.

What you should not do is leave an open preview that is your unprotected production bundle under a different hostname.

CI artifacts, registries, and "just for debugging"

actions/upload-artifact with dist/ or .next/static is useful when a Playwright run fails. It is also a zip of your client JavaScript, often retained for 30 or 90 days, on a public workflow if the repository is public.

The same class of problem shows up elsewhere:

  • Public or organization-visible GitHub Releases that attach dist.zip.
  • npm pack tarballs that include unminified or unobfuscated source because files in package.json was too broad, or because you published a src/ folder "for source maps."
  • Container images pushed to a registry with the build output in a layer, especially if the image is public or old tags can be pulled anonymously.
  • Storybook or Chromatic builds that import the same proprietary module the production app uses.

A practical check after a pipeline change:

# in the CI workspace, after build
find dist .next/static .output/public -name "*.js" | head

# if you upload artifacts, list what is actually in the zip
unzip -l dist-artifact.zip | head

# if you publish a package
npm pack --dry-run

Then ask: if this file were posted on a public URL for a week, would I be comfortable? If not, do not upload it, restrict who can download it, or protect it first.

Source maps and debug builds in CI

A related leak is the debug build you generate "just to inspect a failed test." Source maps with sourcesContent, unminified chunks, and NODE_ENV=development bundles sometimes get published to the same preview hostname, or attached as artifacts named sourcemaps.zip.

Keep maps for your crash tracker, private and separate. Do not put .map files next to JavaScript on a public preview. Do not upload unprotected source maps as a public workflow artifact.

A policy that actually holds

Treat every output that contains your client JavaScript as a shipping surface, not only the production CDN.

For the functions that still have to run in the browser in production, minify everything, obfuscate your own modules if you need a higher floor, and reserve VM-style protection for the short list of algorithms that would hurt you if they were copied.

A JS VM obfuscator compiles selected functions to a custom bytecode and ships a small interpreter, so the original control flow is not sitting in the chunk waiting for a beautifier. VEILVM is a browser-based JSVMP workspace you can point at one extracted function before you wire protection into CI. Interpreted code is slower, so extract the sensitive function, protect that function, and leave the rest of the app alone.

Protection raises the cost of casual inspection and wholesale reuse. It does not help if an unprotected copy of the same function is also hanging off a preview URL.

Checklist

  • [ ] Production builds run the protection step, and you can point to the job that does it.
  • [ ] Preview deployments are authenticated, or protected the same way, or deliberately stripped of the proprietary client engine.
  • [ ] CI artifacts that include dist/ or bundler chunks are private, short-lived, or not uploaded.
  • [ ] npm pack --dry-run and container layers do not include unprotected source you would not put on the CDN.
  • [ ] Source maps are not publicly downloadable from previews or old deploys.
  • [ ] Old preview URLs and previous deploy artifacts expire.

If you want to try VM protection on a single extracted function, VEILVM runs in the browser with no sign-up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds an API so the protection step can run in CI. Standard requests are processed in memory and are not written to logs. Start at https://vm.ldvmp.com.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.