Pre-launch: dev mnemonic literal in frontend source ships to prod bundle #118

Closed
opened 2026-04-26 06:02:59 +00:00 by Brouie · 19 comments
Brouie commented 2026-04-26 06:02:59 +00:00 (Migrated from gitlab.com)

Found during DEX security checklist gap-fill on Sunday 2026-04-26.

Repro

frontend-dapp/src/services/terraclassic/devWallet.ts L4-5:

const DEFAULT_DEV_MNEMONIC =
  'notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius'

L20: const mnemonic = import.meta.env.VITE_DEV_MNEMONIC || DEFAULT_DEV_MNEMONIC

The runtime use is gated by if (!DEV_MODE) throw at L13-15 (DEV_MODE = import.meta.env.VITE_DEV_MODE === 'true' per utils/constants.ts L29) — so prod users can't actually call createDevTerraWallet(). But:

  1. The string literal DEFAULT_DEV_MNEMONIC is a constant in source. Vite bundles it into prod dist/ regardless of the runtime guard.
  2. Any VITE_* env var (including VITE_DEV_MNEMONIC) is inlined by Vite at build time. If a developer ever has VITE_DEV_MNEMONIC set in their environment when running npm run build, that real mnemonic ships to all users of that build.
  3. With #117 (sourcemap: true) also outstanding, the constant is trivially readable in prod.

On-chain check

Address terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v derived from the default mnemonic is a known dev wallet. Current balance via terra-classic-lcd: 6 uidr + 3 umnt (dust, no real funds). So the immediate-loss risk is zero — this is a pre-launch hygiene issue, not an active vulnerability.

Why it still matters pre-launch

  • Sets a bad pattern (mnemonic-in-source). Future devs may copy or extend it without realising it ships to clients.
  • The runtime guard (!DEV_MODE throw) does not strip the string from the bundle — it only blocks the wallet-construction codepath.
  • Bundling VITE_DEV_MNEMONIC env-var fallback chains a second exposure: anyone running a prod build with that env set ships their real mnemonic to all users.
  • Defence-in-depth: with #117 sourcemaps off + this fix, prod bundle has zero dev-wallet trace.

Fix options

Option A (simplest, recommended): split dev wallet into a separate module that's only imported in dev/test builds.

// frontend-dapp/src/services/terraclassic/devWallet.ts
// (file kept but only imported behind a build-time guard)

Plus:

// importer side
if (import.meta.env.DEV) {
  const { createDevTerraWallet } = await import('./devWallet')
  // ...
}

Vite tree-shakes the dynamic import out of prod builds when import.meta.env.DEV is false at build time.

Option B (minimal change): keep the file, but move the literal mnemonic to a .env.development-only var with no fallback. Throw if missing in non-prod, refuse to build if present in prod.

const DEFAULT_DEV_MNEMONIC = ''  // remove literal entirely
const mnemonic = import.meta.env.VITE_DEV_MNEMONIC
if (!mnemonic) throw new Error('VITE_DEV_MNEMONIC required for dev wallet')

Plus a build-time check that fails CI if VITE_DEV_MNEMONIC is set in any prod build context.

Severity

LOW — current dev wallet is a dust address, no real funds, runtime guard blocks usage. But worth fixing pre-mainnet to match the security maturity of bridge + YO frontends, neither of which ship dev mnemonics in their bundles.

Acceptance

  • devWallet.ts no longer ships the literal mnemonic in prod bundles (verified via grep -F 'notice oak worry' dist/assets/*.js → empty)
  • DEX security checklist row 1.5 (secret leakage) updated in cl8y-ecosystem-qa specs/DEX-Security-Checklist-DRAFT.md

cc @PlasticDigits

Found during DEX security checklist gap-fill on Sunday 2026-04-26. ## Repro `frontend-dapp/src/services/terraclassic/devWallet.ts` L4-5: ``` const DEFAULT_DEV_MNEMONIC = 'notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius' ``` L20: `const mnemonic = import.meta.env.VITE_DEV_MNEMONIC || DEFAULT_DEV_MNEMONIC` The runtime use is gated by `if (!DEV_MODE) throw` at L13-15 (`DEV_MODE = import.meta.env.VITE_DEV_MODE === 'true'` per utils/constants.ts L29) — so prod users can't actually call `createDevTerraWallet()`. But: 1. The **string literal** `DEFAULT_DEV_MNEMONIC` is a constant in source. Vite bundles it into prod `dist/` regardless of the runtime guard. 2. Any `VITE_*` env var (including `VITE_DEV_MNEMONIC`) is **inlined** by Vite at build time. If a developer ever has `VITE_DEV_MNEMONIC` set in their environment when running `npm run build`, that real mnemonic ships to all users of that build. 3. With #117 (sourcemap: true) also outstanding, the constant is trivially readable in prod. ## On-chain check Address `terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v` derived from the default mnemonic is a known dev wallet. Current balance via terra-classic-lcd: `6 uidr` + `3 umnt` (dust, no real funds). So the immediate-loss risk is zero — this is a pre-launch hygiene issue, not an active vulnerability. ## Why it still matters pre-launch - Sets a bad pattern (mnemonic-in-source). Future devs may copy or extend it without realising it ships to clients. - The runtime guard (`!DEV_MODE` throw) does not strip the string from the bundle — it only blocks the wallet-construction codepath. - Bundling `VITE_DEV_MNEMONIC` env-var fallback chains a second exposure: anyone running a prod build with that env set ships their real mnemonic to all users. - Defence-in-depth: with #117 sourcemaps off + this fix, prod bundle has zero dev-wallet trace. ## Fix options Option A (simplest, recommended): split dev wallet into a separate module that's only imported in dev/test builds. ``` // frontend-dapp/src/services/terraclassic/devWallet.ts // (file kept but only imported behind a build-time guard) ``` Plus: ``` // importer side if (import.meta.env.DEV) { const { createDevTerraWallet } = await import('./devWallet') // ... } ``` Vite tree-shakes the dynamic import out of prod builds when `import.meta.env.DEV` is `false` at build time. Option B (minimal change): keep the file, but move the literal mnemonic to a `.env.development`-only var with no fallback. Throw if missing in non-prod, refuse to build if present in prod. ``` const DEFAULT_DEV_MNEMONIC = '' // remove literal entirely const mnemonic = import.meta.env.VITE_DEV_MNEMONIC if (!mnemonic) throw new Error('VITE_DEV_MNEMONIC required for dev wallet') ``` Plus a build-time check that fails CI if `VITE_DEV_MNEMONIC` is set in any prod build context. ## Severity LOW — current dev wallet is a dust address, no real funds, runtime guard blocks usage. But worth fixing pre-mainnet to match the security maturity of bridge + YO frontends, neither of which ship dev mnemonics in their bundles. ## Acceptance - [ ] devWallet.ts no longer ships the literal mnemonic in prod bundles (verified via `grep -F 'notice oak worry' dist/assets/*.js` → empty) - [ ] DEX security checklist row 1.5 (secret leakage) updated in cl8y-ecosystem-qa specs/DEX-Security-Checklist-DRAFT.md cc @PlasticDigits
PlasticDigits commented 2026-04-27 03:02:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 1c2d1bb72d

mentioned in commit 1c2d1bb72ded506a685419b18aa30ae5f8d30c04
PlasticDigits commented 2026-04-27 03:02:36 +00:00 (Migrated from gitlab.com)

Fix implemented (option B) — @brouie please verify

Merged to main: 1c2d1bb (dev mnemonic removed from client source, production build guard, gitleaks rule, docs + skills/AGENTS_BUNDLE_DEV_WALLET.md).

What changed

  • devWallet.ts: no default BIP39 literal. VITE_DEV_MNEMONIC is required (trimmed) when VITE_DEV_MODE=true and the Simulated Wallet is used. Address shown in the UI is devWallet.address (no hardcoded bech32).
  • Vite: production vite build throws if VITE_DEV_MNEMONIC is set in the merged loadEnv('production', …) (covers .env, .env.local, .env.production, and shell).
  • deploy-dex-local.sh: writes the LocalTerra test phrase to frontend-dapp/.env.development (same value as TEST_MNEMONIC in docker/init-chain.sh) so it is not loaded for default production builds.
  • Playwright: webServer.env sets VITE_DEV_MNEMONIC from docker/init-chain.sh via e2e/localterra-mnemonic.ts (no duplicate in src/).
  • Gitleaks: new rule bip39-like-phrase-frontend-src (12+ quoted lowercase 3–8 letter words) scoped to frontend-dapp/src/*.ts(x) with *.test|spec allowlist. Pre-commit: gitleaks protect --staged -c .gitleaks.toml. Why it did not block before: default gitleaks rules do not model BIP39 word lists; gitleaks detect on full history can still list old commits — use gitleaks detect --no-git or pre-commit for current sources.

Ecosystem: acceptance asked for cl8y-ecosystem-qa DEX-Security-Checklist-DRAFT.md row 1.5 — that file lives in another project; I did not open an MR there from this repo.

Issue left open for your sign-off.

Checklist for verification

  • cd frontend-dapp && npm run build succeeds on a clean tree without VITE_DEV_MNEMONIC in env or .env* (production).
  • VITE_DEV_MNEMONIC=x npm run build in frontend-dapp fails with the GitLab #118 error.
  • grep -rF 'notice oak worry' dist/assets/*.js is empty after npm run build.
  • VITE_DEV_MODE=true + VITE_DEV_MNEMONIC in .env.development (or from deploy-dex-local.sh output): Simulated Wallet connects and shows the expected address for that mnemonic.
  • gitleaks protect --staged -c .gitleaks.toml clean when staging a normal ts change; re-staging a 12+ word BIP39-like string in src/ (non-test) should be flagged.
  • (Optional) Run Playwright test:e2e in your environment with LocalTerra if you rely on Simulated Wallet in E2E.

Leaving open as requested; tagging for verification.

## Fix implemented (option B) — @brouie please verify **Merged to `main`:** `1c2d1bb` (dev mnemonic removed from client source, production build guard, gitleaks rule, docs + `skills/AGENTS_BUNDLE_DEV_WALLET.md`). ### What changed - **`devWallet.ts`:** no default BIP39 literal. `VITE_DEV_MNEMONIC` is required (trimmed) when `VITE_DEV_MODE=true` and the Simulated Wallet is used. Address shown in the UI is `devWallet.address` (no hardcoded bech32). - **Vite:** production `vite build` **throws** if `VITE_DEV_MNEMONIC` is set in the merged `loadEnv('production', …)` (covers `.env`, `.env.local`, `.env.production`, and shell). - **`deploy-dex-local.sh`:** writes the LocalTerra test phrase to **`frontend-dapp/.env.development`** (same value as `TEST_MNEMONIC` in `docker/init-chain.sh`) so it is not loaded for default production builds. - **Playwright:** `webServer.env` sets `VITE_DEV_MNEMONIC` from `docker/init-chain.sh` via `e2e/localterra-mnemonic.ts` (no duplicate in `src/`). - **Gitleaks:** new rule `bip39-like-phrase-frontend-src` (12+ quoted lowercase 3–8 letter words) scoped to `frontend-dapp/src/*.ts(x)` with `*.test|spec` allowlist. Pre-commit: `gitleaks protect --staged -c .gitleaks.toml`. **Why it did not block before:** default gitleaks rules do not model BIP39 word lists; `gitleaks detect` on **full history** can still list old commits — use `gitleaks detect --no-git` or pre-commit for current sources. **Ecosystem:** acceptance asked for `cl8y-ecosystem-qa` `DEX-Security-Checklist-DRAFT.md` row 1.5 — that file lives in another project; I did not open an MR there from this repo. **Issue left open** for your sign-off. ### Checklist for verification - [ ] `cd frontend-dapp && npm run build` succeeds on a clean tree **without** `VITE_DEV_MNEMONIC` in env or `.env*` (production). - [ ] `VITE_DEV_MNEMONIC=x npm run build` in `frontend-dapp` **fails** with the GitLab #118 error. - [ ] `grep -rF 'notice oak worry' dist/assets/*.js` is **empty** after `npm run build`. - [ ] `VITE_DEV_MODE=true` + `VITE_DEV_MNEMONIC` in `.env.development` (or from `deploy-dex-local.sh` output): Simulated Wallet connects and shows the expected address for that mnemonic. - [ ] `gitleaks protect --staged -c .gitleaks.toml` clean when staging a normal `ts` change; re-staging a 12+ word BIP39-like string in `src/` (non-test) should be flagged. - [ ] (Optional) Run Playwright `test:e2e` in your environment with LocalTerra if you rely on Simulated Wallet in E2E. *Leaving open as requested; tagging for verification.*
Brouie commented 2026-05-01 13:05:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #117

mentioned in issue #117
Brouie commented 2026-05-01 13:14:48 +00:00 (Migrated from gitlab.com)

Verified on 1c2d1bb. 5/6 PASS (item 4 browser deferred; item 5 done at source-review level since gitleaks binary is not on the QA server). Leaving open for browser walkthrough.

Acceptance items

  • Item 1: clean prod build with no VITE_DEV_MNEMONIC — npm run build succeeded in 13.07s on a clean tree (.env.local had VITE_DEV_MODE=true only, no VITE_DEV_MNEMONIC; shell env clean). Build output complete with all expected chunks.
  • Item 2: VITE_DEV_MNEMONIC=x npm run build fails with #118 error — guard fires correctly:
    Error: VITE_DEV_MNEMONIC must not be set for production builds — it would be inlined into the client bundle. Remove it from .env, .env.local, .env.production, and your shell (GitLab #118).
    
    Build aborts at config-load time, before any bundle is written. Guard is command === 'build' && mode === 'production' scoped, uses loadEnv(mode, ..., 'VITE_') so it covers .env, .env.local, .env.production, and shell.
  • Item 3: grep -F 'notice oak worry' dist/assets/*.js — 0 hits on fresh prod build. Source file frontend-dapp/src/services/terraclassic/devWallet.ts no longer contains the literal either (grep -F confirms REMOVED).
  • Item 4: Simulated Wallet connects with .env.development mnemonic — DEFERRED, needs browser session. Will run with a stack-up session.
  • Item 5: gitleaks rule — done at source-review level (gitleaks binary not installed on QA server, no install per server rules). Rule definition in .gitleaks.toml audited:
    • id = "bip39-like-phrase-frontend-src" (matches issue text)
    • regex = '''['\"]([a-z]{3,8}(?:\s[a-z]{3,8}){11,})['\"]''' — 12+ lowercase words of length 3-8, BIP39 word-length range correct.
    • path = '''(?i)(frontend-dapp|frontend)/src/.*\.(ts|tsx)$''' — scoped to app source.
    • Allowlist for *.test.ts(x), *.spec.ts(x) — fixtures don't trip the rule (correct, dev-wallet tests need mnemonic strings).
    • Top-level allowlist for terra1[a-z0-9]{38,} and Secp256k1PubKey — sensible exclusions.
    • Regex sanity-check against the original literal from #118 confirmed via echo "'notice oak worry ... genius'" | grep -E "..." → MATCHES. Rule would have caught the original leak.
  • Item 6: Playwright e2e — DEFERRED, optional per your note.

Out-of-repo task

  • ecosystem-qa DEX security checklist row 1.5 → OK — pushed as f2fe6a7 on cl8y-ecosystem-qa main: specs(dex-security): mark row 1.5 secret leakage OK (DEX #118 verified). Row now reads NO — no secrets ship in prod | Fixed by #118 (commit 1c2d1bb)... | OK.

Good layered fix — source clean + build guard + dist clean + gitleaks rule. Will close after item 4 browser walkthrough.

cc @PlasticDigits

Verified on `1c2d1bb`. 5/6 PASS (item 4 browser deferred; item 5 done at source-review level since gitleaks binary is not on the QA server). Leaving open for browser walkthrough. ### Acceptance items - [x] **Item 1: clean prod build with no `VITE_DEV_MNEMONIC`** — `npm run build` succeeded in 13.07s on a clean tree (`.env.local` had `VITE_DEV_MODE=true` only, no `VITE_DEV_MNEMONIC`; shell env clean). Build output complete with all expected chunks. - [x] **Item 2: `VITE_DEV_MNEMONIC=x npm run build` fails with #118 error** — guard fires correctly: ``` Error: VITE_DEV_MNEMONIC must not be set for production builds — it would be inlined into the client bundle. Remove it from .env, .env.local, .env.production, and your shell (GitLab #118). ``` Build aborts at config-load time, before any bundle is written. Guard is `command === 'build' && mode === 'production'` scoped, uses `loadEnv(mode, ..., 'VITE_')` so it covers `.env`, `.env.local`, `.env.production`, and shell. - [x] **Item 3: `grep -F 'notice oak worry' dist/assets/*.js`** — 0 hits on fresh prod build. Source file `frontend-dapp/src/services/terraclassic/devWallet.ts` no longer contains the literal either (`grep -F` confirms REMOVED). - [ ] **Item 4: Simulated Wallet connects with `.env.development` mnemonic** — DEFERRED, needs browser session. Will run with a stack-up session. - [x] **Item 5: gitleaks rule** — done at source-review level (gitleaks binary not installed on QA server, no install per server rules). Rule definition in `.gitleaks.toml` audited: - `id = "bip39-like-phrase-frontend-src"` (matches issue text) - `regex = '''['\"]([a-z]{3,8}(?:\s[a-z]{3,8}){11,})['\"]'''` — 12+ lowercase words of length 3-8, BIP39 word-length range correct. - `path = '''(?i)(frontend-dapp|frontend)/src/.*\.(ts|tsx)$'''` — scoped to app source. - Allowlist for `*.test.ts(x)`, `*.spec.ts(x)` — fixtures don't trip the rule (correct, dev-wallet tests need mnemonic strings). - Top-level allowlist for `terra1[a-z0-9]{38,}` and `Secp256k1PubKey` — sensible exclusions. - **Regex sanity-check against the original literal** from #118 confirmed via `echo "'notice oak worry ... genius'" | grep -E "..."` → MATCHES. Rule would have caught the original leak. - [ ] **Item 6: Playwright e2e** — DEFERRED, optional per your note. ### Out-of-repo task - [x] **ecosystem-qa DEX security checklist row 1.5 → OK** — pushed as `f2fe6a7` on `cl8y-ecosystem-qa` main: `specs(dex-security): mark row 1.5 secret leakage OK (DEX #118 verified)`. Row now reads `NO — no secrets ship in prod | Fixed by #118 (commit 1c2d1bb)... | OK`. Good layered fix — source clean + build guard + dist clean + gitleaks rule. Will close after item 4 browser walkthrough. cc @PlasticDigits
PlasticDigits commented 2026-05-02 04:49:35 +00:00 (Migrated from gitlab.com)

@Brouie pending 4 browser walkthru to close

@Brouie pending 4 browser walkthru to close
Brouie commented 2026-05-05 23:55:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #133

mentioned in issue #133
Brouie commented 2026-05-06 04:20:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #121

mentioned in issue #121
Brouie commented 2026-05-07 05:13:30 +00:00 (Migrated from gitlab.com)

@PlasticDigits @totdking — last open item is the browser walk on Simulated Wallet (item 4 from acceptance). since totdking is now driving DEX visual QA, tagging him for the on-stack walkthrough since he likely has the LocalTerra + frontend running.

totdking — quick check when convenient: with VITE_DEV_MODE=true and a valid VITE_DEV_MNEMONIC in frontend-dapp/.env.development (or via deploy-dex-local.sh), confirm Simulated Wallet connects in the dapp and shows the expected address derived from that mnemonic. screenshot of the wallet panel is enough.

@PlasticDigits @totdking — last open item is the browser walk on Simulated Wallet (item 4 from acceptance). since totdking is now driving DEX visual QA, tagging him for the on-stack walkthrough since he likely has the LocalTerra + frontend running. totdking — quick check when convenient: with `VITE_DEV_MODE=true` and a valid `VITE_DEV_MNEMONIC` in `frontend-dapp/.env.development` (or via `deploy-dex-local.sh`), confirm Simulated Wallet connects in the dapp and shows the expected address derived from that mnemonic. screenshot of the wallet panel is enough.
totdking commented 2026-05-07 11:36:36 +00:00 (Migrated from gitlab.com)

Wallet from front end

image.png{width=900 height=545}

Terminal derivation from mnemonic

image.png{width=437 height=600}

Works as expected @Brouie

Wallet from front end ![image.png](/uploads/f8c2f11b3b9adcb38232db9d2e67b230/image.png){width=900 height=545} Terminal derivation from mnemonic ![image.png](/uploads/b6ffcf66b2833d7ba5550ff48f9fed07/image.png){width=437 height=600} Works as expected @Brouie
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-07 12:03:31 +00:00
PlasticDigits commented 2026-05-07 12:03:32 +00:00 (Migrated from gitlab.com)

@totdking Do not upload screenshots including partial mnemonics (this is a dev mnemonic, but establishing best practtice & habits). please remove from your note. However, issue is closed.

@totdking Do not upload screenshots including partial mnemonics (this is a dev mnemonic, but establishing best practtice & habits). please remove from your note. However, issue is closed.
Brouie commented 2026-06-10 06:17:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
PlasticDigits commented 2026-06-12 11:10:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #372

mentioned in issue #372
PlasticDigits commented 2026-06-13 07:09:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #376

mentioned in issue #376
PlasticDigits commented 2026-06-13 10:18:24 +00:00 (Migrated from gitlab.com)

mentioned in merge request !904

mentioned in merge request !904
PlasticDigits commented 2026-06-25 14:13:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #421

mentioned in issue #421
PlasticDigits commented 2026-08-28 09:24:20 +00:00 (Migrated from gitlab.com)

mentioned in issue #695

mentioned in issue #695
PlasticDigits commented 2026-08-28 09:24:20 +00:00 (Migrated from gitlab.com)

marked as related to #695

marked as related to #695
PlasticDigits commented 2026-08-28 09:39:04 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1192

mentioned in merge request !1192
PlasticDigits commented 2026-08-30 02:48:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #706

mentioned in issue #706
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#118
No description provided.