A camera login page leaked admin access to Hanwha's GitHub
The culprit is a one-line Vite build footgun that bakes your entire CI environment into shipped code.
The bug isn't the camera — it's the build step
A researcher pulled apart the firmware of a Hanwha Vision Wisenet camera — the XNP-9300RW, a several-thousand-dollar PTZ unit you'd find bolted to a stadium or a data-center ceiling — and found a GitHub token sitting in the admin login page. Not an API key scoped to a telemetry endpoint. A token with admin rights to hundreds of repositories in Hanwha's GitHub organization, shipped inside the web UI that every one of these cameras serves to anyone who browses to its IP.
The same token appeared, duplicated, across roughly 30 files in the bundle. To Hanwha's credit, once they got the report they revoked it within 12 hours — a response time most vendors can't manage in 12 days. But the fast cleanup is the least interesting part of this story. The interesting part is how an org-admin credential ended up in a static asset served by an embedded device, because the mechanism is a footgun sitting in a huge number of front-end codebases right now, and most of the people holding it don't know it's loaded.
How process.env becomes public
The root cause is a single build-time decision. The camera's admin UI is built with Vite, and somewhere in the config a value was set to the entirety of process.env at build time. If you've written front-end tooling you've probably seen the pattern that does this:
// vite.config.js — do not do this
export default defineConfig({
define: {
'process.env': process.env,
},
})
It looks harmless. Some library expects process.env to exist in the browser, so you hand it the whole object and move on. But define does literal text substitution at build time. Every reference to process.env in the bundle gets replaced with a serialized copy of the environment the build ran in — and the build ran in CI. So whatever the CI job had in scope (deploy keys, registry tokens, and in this case a GitHub PAT) gets inlined as a string literal into the JavaScript, minified, and shipped. Tree-shaking won't save you; the value is a constant, not an import. Nobody has to import the secret for it to travel.
This is not a Hanwha-specific screwup. Sprocket Security documented the same class of bug reaching full CI/CD compromise — live AWS keys and a CircleCI token in a client bundle, then S3, then more GitHub tokens, then everything. Vite's own env documentation tries to fence this off: only VITE_-prefixed variables are meant to reach the client. But the prefix is a gate for exposure, not a certificate of safety — a VITE_STRIPE_SECRET_KEY ships just as happily as a public one — and the define: { 'process.env': process.env } route bypasses the fence entirely.
Firmware makes it strictly worse
Web apps have this problem too. The difference is that a web app's bundle lives on a server you control, and when you find a leaked secret you rotate it and redeploy in an afternoon. Firmware is the opposite of that on every axis.
The bundle ships inside the product. Anyone who owns the hardware — or, as here, downloads the firmware image from the vendor's public support portal — holds a permanent, offline copy. You can't unship it. The researcher didn't need physical access or a zero-day; they downloaded roughly 500 firmware images from Hanwha's site and got usable filesystems out of about 62% of them. Getting there took real work: the images are AES-256-CBC-encrypted nested tarballs, and the key is XOR'd against a static table inside an fwupgrader binary and reassembled at runtime. Reverse-engineering that (with an LLM's help reading the disassembly) coughed up the key and IV. But "hard to extract" is not a security boundary. It's a speed bump in front of a scanner, and trufflehog does the rest in seconds.
Then the multiplier: the same token turned up in three different firmware versions. One leak, then a copy-paste of the mistake across releases, each one baking the same live credential into a new batch of shipped devices. Every camera on that firmware is a distribution point for a key that unlocks the source code of the company that made it.
What to actually do about it
If you build a client bundle — web or embedded — the operational rule is blunt: treat everything in your client bundle as public, and treat your CI environment as the thing most likely to leak into it. Concretely:
- Never
defineprocess.envwholesale. If a dependency demandsprocess.envin the browser, hand it an explicit allowlist of non-secret keys, not the whole object. Same discipline forwebpack.DefinePluginand esbuild's--define; this isn't a Vite-only trap. - Scan the output, not just the repo. Most teams point secret scanners at git history and stop. Run trufflehog against the built artifact — the
dist/folder, the firmware image, the container layer — because that's wheredefinesubstitution actually happens. A clean repo can produce a dirty bundle. - Make it a CI gate. A scan of the shipped artifact that fails the build on a hit would have caught this before a single camera left the factory, and it's a few lines in a pipeline.
- Scope build credentials to the build. A CI job that compiles a web UI has no business holding an org-admin GitHub token. Fine-grained, short-lived, single-repo tokens turn "catastrophic" into "annoying" when — not if — one escapes.
The camera worked fine. It filmed what it was pointed at. The failure was upstream, in a build config that quietly decided the developers' environment and the public's were the same thing. That decision is one line, it's in a lot of repos, and firmware is where it stops being recoverable. Go grep your Vite configs for 'process.env' before someone else greps your firmware.
Sources & further reading
- My security camera shipped a GitHub admin token in its login page — hhh.hn
- Hunting Secrets in JavaScript: Vite Misconfiguration Leads to Full CI/CD Compromise — sprocketsecurity.com
- Env Variables and Modes — vite.dev
- TruffleHog — github.com
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 5
been there. had a junior dev in '09 dump database credentials into a compiled .jar we shipped with an appliance. took six months and a support call from an angry customer to find it. the fun part was realizing how many were already out in the field. vite's just the new name for the same old mistake—treating your build output like it's disposable when it absolutely isn't.
yeah, the scary part is how much easier vite makes this particular mistake. env vars in the browser bundle feel almost inevitable now unless you're paranoid about it.
the vite thing is real—we had something similar creep into a build artifact at my project last year when someone copied a `.env` file into the public dir by accident. the fix was trivial but the realization that ci vars were baked into every release was a gut punch. hanwha's bigger problem is that nobody apparently ever pulled the firmware apart locally or looked at the bundle contents before shipping it to thousands of devices, which is its own kind of exhausting.
Vite baking CI env vars into client bundles is such a classic footgun. at least they revoked it fast, but yeah—admin token in 30 files across shipped firmware is rough.
revocation is the band-aid though—the real problem is that nobody's auditing what ends up in production builds. one token this time, could be secrets for everything next time. this needs pre-commit hooks or artifact scanning, not just better env var handling.