From d6c1f727fa90b647759dd82df39d06e9e3570b37 Mon Sep 17 00:00:00 2001 From: success-OG Date: Mon, 20 Jul 2026 17:14:41 +0100 Subject: [PATCH] Harden environment variable and secrets management --- .env.example | 23 +++++ .gitignore | 14 +++ Documents/Task Bounty/.env.example | 36 +++++--- Documents/Task Bounty/.gitignore | 4 +- ENVIRONMENT.md | 101 ++++++++++++++++++++++ SECURITY.md | 6 +- SETUP.md | 27 ++++-- contract/.gitignore | 7 +- frontend/.env.example | 30 +++++++ frontend/.gitignore | 4 +- frontend/README.md | 6 ++ frontend/src/hooks/stellar-wallets-kit.ts | 9 +- frontend/src/lib/env.test.ts | 64 ++++++++++++++ frontend/src/lib/env.ts | 60 +++++++++++++ frontend/src/lib/stellar.ts | 9 +- 15 files changed, 373 insertions(+), 27 deletions(-) create mode 100644 .env.example create mode 100644 ENVIRONMENT.md create mode 100644 frontend/.env.example create mode 100644 frontend/src/lib/env.test.ts create mode 100644 frontend/src/lib/env.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b213d91 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# ============================================================================= +# Task-Bounty root environment template +# ============================================================================= +# This repository has two active workspaces. Prefer workspace-local env files: +# +# frontend/.env.example → copy to frontend/.env.local +# Documents/Task Bounty/.env.example → legacy reference only (not used by app) +# +# DO NOT store real private keys, seed phrases, or production API secrets here. +# DO NOT commit any file named `.env`, `.env.local`, or `.env.*.local`. +# ============================================================================= + +# --- Frontend (public / safe to expose in the browser) --- +NEXT_PUBLIC_STELLAR_NETWORK=PUBLIC +NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org +NEXT_PUBLIC_SOROBAN_RPC_URL= +NEXT_PUBLIC_CONTRACT_ID= + +# --- Deploy / ops (server or local shell ONLY — never NEXT_PUBLIC_) --- +# Populate these in your shell or a gitignored `.env.local`, never in git. +# STELLAR_SECRET_KEY= +# STELLAR_NETWORK_PASSPHRASE= +# DEPLOYER_SECRET_KEY= diff --git a/.gitignore b/.gitignore index 6c734b7..9382702 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,16 @@ # Generated dependency vulnerability scan reports (local + CI artifacts) reports/ + +# Environment / secrets — never commit real credentials +.env +.env.* +!.env.example +**/.env +**/.env.* +!**/.env.example + +# Common secret material +*.pem +*.key +id_rsa +id_ed25519 diff --git a/Documents/Task Bounty/.env.example b/Documents/Task Bounty/.env.example index 5e0264f..c14c5fd 100644 --- a/Documents/Task Bounty/.env.example +++ b/Documents/Task Bounty/.env.example @@ -1,17 +1,27 @@ -# Private key for deployment (DO NOT commit actual private key) -PRIVATE_KEY=your_private_key_here +# ============================================================================= +# LEGACY / REFERENCE ONLY — not consumed by the active Paymesh frontend +# ============================================================================= +# The live app uses Stellar/Soroban (see ../../frontend/.env.example). +# This file remains for historical TaskBounty EVM docs under Documents/. +# +# NEVER commit real keys. Replace placeholders locally in a gitignored `.env`. +# ============================================================================= -# RPC URLs -MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key -SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-api-key +# Deployer / signer secret for local scripts (KEEP OUT OF GIT) +# Example shape only — leave blank in shared templates: +PRIVATE_KEY= -# Etherscan API key for contract verification -ETHERSCAN_API_KEY=your_etherscan_api_key +# Public RPC endpoints (use your own provider URL; do not commit API keys) +MAINNET_RPC_URL=https://eth-mainnet.example/v2/ +SEPOLIA_RPC_URL=https://eth-sepolia.example/v2/ -# Deployed contract addresses (update after deployment) -BOUNTY_ADDRESS=0x... -RESOLVER_ADDRESS=0x... -FACTORY_ADDRESS=0x... +# Block explorer verification (optional) +ETHERSCAN_API_KEY= -# Arbitrator address (defaults to deployer if not set) -ARBITRATOR=0x... +# Deployed contract addresses (public — fill after deploy) +BOUNTY_ADDRESS= +RESOLVER_ADDRESS= +FACTORY_ADDRESS= + +# Optional arbitrator address (public) +ARBITRATOR= diff --git a/Documents/Task Bounty/.gitignore b/Documents/Task Bounty/.gitignore index 783b135..78447b6 100644 --- a/Documents/Task Bounty/.gitignore +++ b/Documents/Task Bounty/.gitignore @@ -10,8 +10,10 @@ out/ # Docs docs/ -# Dotenv file +# Dotenv files — never commit real secrets .env +.env.* +!.env.example # Dependencies lib/ diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md new file mode 100644 index 0000000..7fabc33 --- /dev/null +++ b/ENVIRONMENT.md @@ -0,0 +1,101 @@ +# Environment Variable Management + +This document describes how Task-Bounty manages configuration and secrets so that +sensitive values are never committed or leaked to the browser. + +## Acceptance checklist + +- [x] `.env.example` templates updated (root + frontend + legacy docs) +- [x] Sensitive values removed from committed templates +- [x] Documentation for safe env usage (this file) + +## Source of truth + +| File | Purpose | Commit? | +|------|---------|---------| +| [`frontend/.env.example`](frontend/.env.example) | Active Next.js app template | Yes | +| [`.env.example`](.env.example) | Root overview / pointers | Yes | +| [`Documents/Task Bounty/.env.example`](Documents/Task%20Bounty/.env.example) | Legacy EVM reference only | Yes | +| `frontend/.env.local` / `.env` / `.env.*.local` | Real local secrets & overrides | **Never** | + +## Rules + +1. **Never commit secrets.** Private keys, seed phrases, deployer secrets, and + provider API tokens must live only in gitignored files or a secret manager. +2. **Never put secrets in `NEXT_PUBLIC_*` variables.** Anything with that prefix + is bundled into client JavaScript and is publicly readable. +3. **Use placeholders in examples.** Templates may show empty values or + `` markers — never real credentials. +4. **Prefer workspace-local files.** Frontend config belongs in `frontend/.env.local`. +5. **Defaults must be safe.** The app boots without a local env file by using + public Stellar defaults (Horizon mainnet URL, `PUBLIC` network). + +## Frontend variables + +| Variable | Public? | Description | +|----------|---------|-------------| +| `NEXT_PUBLIC_STELLAR_NETWORK` | Yes | `PUBLIC` or `TESTNET` for the wallet kit | +| `NEXT_PUBLIC_HORIZON_URL` | Yes | Horizon HTTP base URL for account reads | +| `NEXT_PUBLIC_SOROBAN_RPC_URL` | Yes | Optional Soroban RPC URL (future contract calls) | +| `NEXT_PUBLIC_CONTRACT_ID` | Yes | Optional deployed contract ID (`C...`) | +| `NODE_ENV` | Yes | Set by Next.js / Node — do not override with secrets | + +Server-only secrets (if introduced later) must: + +- omit the `NEXT_PUBLIC_` prefix +- be read only inside Route Handlers / Server Components / server utilities +- never be logged or returned in API responses + +## Local setup + +```bash +cd frontend +cp .env.example .env.local +# edit .env.local with non-secret public overrides as needed +pnpm dev +``` + +Confirm `.env.local` is ignored: + +```bash +git check-ignore -v frontend/.env.local +``` + +## Contract / deploy secrets + +Soroban deploy keys and network secrets belong in your shell environment or a +password manager — not in the frontend bundle: + +```bash +# Example: export for a single shell session (do not commit) +export STELLAR_SECRET_KEY="S..." # your key — never paste into git +stellar contract deploy --source-account "$STELLAR_SECRET_KEY" ... +``` + +The AutoShare contract workspace under `contract/` does not require a committed +`.env` file for `cargo test` / `cargo build`. + +## What was removed / hardened + +- Legacy `Documents/Task Bounty/.env.example` no longer embeds sample Alchemy URL + paths that looked like filled API slots; keys are blank placeholders. +- `PRIVATE_KEY` and explorer API keys are empty in templates. +- Frontend gitignore allows committing `.env.example` while still ignoring + `.env`, `.env.local`, and `.env.*.local`. +- Root gitignore blocks common secret env filenames repo-wide. + +## Verification helpers + +```bash +# Ensure example files contain no high-entropy secret-looking assignments +cd frontend && pnpm test -- src/lib/env.test.ts + +# Production build must succeed with defaults (no .env.local required) +pnpm build +``` + +## Related docs + +- [SECURITY.md](SECURITY.md) — vulnerability reporting and product security posture +- [SETUP.md](SETUP.md) — local install and run instructions +- [SECURITY_HEADERS.md](SECURITY_HEADERS.md) — HTTP header configuration diff --git a/SECURITY.md b/SECURITY.md index 174b9b0..fbf7c2c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -68,7 +68,11 @@ We provide security updates for the following versions: - HTTP Security Headers (CSP, HSTS, X-Frame-Options) are enforced on all responses - Input validation on all user-facing forms - Wallet integration follows Stellar Wallets Kit security best practices -- Environment variables are never exposed in frontend code +- Environment variable management: + - Secrets must never be committed (see [ENVIRONMENT.md](ENVIRONMENT.md)) + - Only `NEXT_PUBLIC_*` values may reach the browser — treat them as public + - Private keys, mnemonics, and API tokens must stay in gitignored `.env.local` files or a secret manager + - Templates live in `.env.example` with empty / placeholder values only ### Blockchain-Specific Concerns diff --git a/SETUP.md b/SETUP.md index d9c7342..359156b 100644 --- a/SETUP.md +++ b/SETUP.md @@ -147,14 +147,29 @@ cd .. ### Frontend configuration -The current frontend does **not** require a `.env.local` file to boot. +Copy the example env file if you want local overrides (optional — defaults work out of the box): -At the moment: +```bash +cd frontend +cp .env.example .env.local +``` + +Public configuration is documented in [ENVIRONMENT.md](ENVIRONMENT.md). Important rules: + +- **Never commit** `.env`, `.env.local`, or real private keys +- **Never put secrets** in `NEXT_PUBLIC_*` variables (they ship to the browser) +- Defaults: Horizon `https://horizon.stellar.org`, wallet network `PUBLIC` + +At the moment the frontend reads: -- wallet network is set in `frontend/src/hooks/stellar-wallets-kit.ts` -- Horizon endpoint is set in `frontend/src/lib/stellar.ts` +| Variable | Used by | +|----------|---------| +| `NEXT_PUBLIC_STELLAR_NETWORK` | `frontend/src/hooks/stellar-wallets-kit.ts` | +| `NEXT_PUBLIC_HORIZON_URL` | `frontend/src/lib/stellar.ts` | +| `NEXT_PUBLIC_SOROBAN_RPC_URL` | reserved for future contract clients | +| `NEXT_PUBLIC_CONTRACT_ID` | reserved for future contract clients | -That means the old file below is **reference-only** and is not consumed by the current frontend: +The legacy file below is **reference-only** and is **not** consumed by the current frontend: ```text Documents/Task Bounty/.env.example @@ -162,7 +177,7 @@ Documents/Task Bounty/.env.example ### When you may still want local configuration -You may choose to add a private `.env.local` later if you wire contract IDs, RPC URLs, or feature flags into the frontend. Until the code reads those variables, creating the file has no effect. +Use `frontend/.env.local` to point at Testnet Horizon, a custom RPC URL, or a deployed contract ID without changing source. Keep deployer secrets in your shell or password manager — not in the frontend env file. --- diff --git a/contract/.gitignore b/contract/.gitignore index a36ad06..d516135 100644 --- a/contract/.gitignore +++ b/contract/.gitignore @@ -4,4 +4,9 @@ target # Local settings .soroban .stellar -test_snapshots \ No newline at end of file +test_snapshots + +# Environment / secrets +.env +.env.* +!.env.example diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..1f12d62 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,30 @@ +# ============================================================================= +# Frontend environment variables (Task-Bounty / Paymesh) +# ============================================================================= +# Copy this file to `.env.local` for local overrides: +# cp .env.example .env.local +# +# Rules: +# - NEVER put secret keys, mnemonics, or private API tokens in this file. +# - NEVER commit `.env`, `.env.local`, or `.env*.local`. +# - Only `NEXT_PUBLIC_*` values are readable in the browser. Treat them as public. +# - Server-only secrets (if added later) must NOT use the NEXT_PUBLIC_ prefix. +# ============================================================================= + +# Stellar network used by the wallet kit. +# Allowed values: PUBLIC | TESTNET +NEXT_PUBLIC_STELLAR_NETWORK=PUBLIC + +# Horizon HTTP API used for account/balance reads (public endpoint). +NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org + +# Optional Soroban RPC endpoint for future contract client wiring. +# Leave empty until the frontend invokes contracts directly. +NEXT_PUBLIC_SOROBAN_RPC_URL= + +# Optional deployed AutoShare / TaskBounty contract ID (C...). +# Leave empty until the UI is wired to on-chain calls. +NEXT_PUBLIC_CONTRACT_ID= + +# Optional app environment label for diagnostics (not a secret). +# NODE_ENV is set automatically by Next.js (development | production | test). diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a52..2e6d414 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,7 +31,9 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env +.env.* +!.env.example # vercel .vercel diff --git a/frontend/README.md b/frontend/README.md index 5d740c4..6106f15 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -72,6 +72,12 @@ Example response (`200 OK`, `Cache-Control: no-store`): The endpoint is always server-rendered (never cached), so it reflects the live process. It can be wired into load balancers, uptime monitors, or container orchestration probes. +## Environment variables + +Copy [`.env.example`](./.env.example) to `.env.local` for optional public overrides (network, Horizon URL, contract ID). + +**Never commit secrets.** See [`../ENVIRONMENT.md`](../ENVIRONMENT.md) for the full policy. Only `NEXT_PUBLIC_*` values are available in the browser — do not put private keys or API tokens there. + ## Running Tests Unit tests are written with [Vitest](https://vitest.dev): diff --git a/frontend/src/hooks/stellar-wallets-kit.ts b/frontend/src/hooks/stellar-wallets-kit.ts index 42ac842..4879363 100644 --- a/frontend/src/hooks/stellar-wallets-kit.ts +++ b/frontend/src/hooks/stellar-wallets-kit.ts @@ -5,6 +5,7 @@ import { WalletNetwork, } from "@creit.tech/stellar-wallets-kit"; import { createError, ErrorCodes } from "@/lib/errors"; +import { getPublicEnv } from "@/lib/env"; const SELECTED_WALLET_ID = "selectedWalletId"; @@ -13,6 +14,12 @@ function getSelectedWalletId() { return localStorage.getItem(SELECTED_WALLET_ID); } +function resolveWalletNetwork() { + return getPublicEnv().stellarNetwork === "TESTNET" + ? WalletNetwork.TESTNET + : WalletNetwork.PUBLIC; +} + let kit: StellarWalletsKit | null = null; function getKit(): StellarWalletsKit | null { @@ -21,7 +28,7 @@ function getKit(): StellarWalletsKit | null { if (typeof window !== "undefined") { kit = new StellarWalletsKit({ modules: allowAllModules(), - network: WalletNetwork.PUBLIC, + network: resolveWalletNetwork(), selectedWalletId: getSelectedWalletId() ?? FREIGHTER_ID, }); return kit; diff --git a/frontend/src/lib/env.test.ts b/frontend/src/lib/env.test.ts new file mode 100644 index 0000000..a7253e6 --- /dev/null +++ b/frontend/src/lib/env.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { getPublicEnv, looksLikeSensitiveSecret } from "@/lib/env"; + +describe("getPublicEnv", () => { + it("returns safe public defaults when env overrides are unset", () => { + const env = getPublicEnv(); + + expect(env.stellarNetwork).toBe("PUBLIC"); + expect(env.horizonUrl).toBe("https://horizon.stellar.org"); + expect(env.sorobanRpcUrl).toBeUndefined(); + expect(env.contractId).toBeUndefined(); + }); +}); + +describe("looksLikeSensitiveSecret", () => { + it("flags stellar secret keys and ethereum private keys", () => { + expect( + looksLikeSensitiveSecret(`S${"A".repeat(55)}`), + ).toBe(true); + expect( + looksLikeSensitiveSecret( + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ), + ).toBe(true); + expect(looksLikeSensitiveSecret("sk-live-abcdefghijklmnopqrstuv")).toBe(true); + }); + + it("allows empty placeholders and public config values", () => { + expect(looksLikeSensitiveSecret("")).toBe(false); + expect(looksLikeSensitiveSecret("your_private_key_here")).toBe(false); + expect(looksLikeSensitiveSecret("https://horizon.stellar.org")).toBe(false); + expect(looksLikeSensitiveSecret("PUBLIC")).toBe(false); + }); +}); + +describe(".env.example templates", () => { + it("do not contain assigned sensitive secrets", () => { + const files = [ + path.resolve(__dirname, "../../.env.example"), + path.resolve(__dirname, "../../../.env.example"), + path.resolve(__dirname, "../../../Documents/Task Bounty/.env.example"), + ]; + + for (const filePath of files) { + const contents = readFileSync(filePath, "utf8"); + const assignments = contents + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#") && line.includes("=")); + + for (const line of assignments) { + const value = line.slice(line.indexOf("=") + 1).trim(); + expect( + looksLikeSensitiveSecret(value), + `${filePath} has a sensitive-looking value: ${line}`, + ).toBe(false); + } + } + }); +}); diff --git a/frontend/src/lib/env.ts b/frontend/src/lib/env.ts new file mode 100644 index 0000000..dec2d9e --- /dev/null +++ b/frontend/src/lib/env.ts @@ -0,0 +1,60 @@ +export type StellarNetworkName = "PUBLIC" | "TESTNET"; + +const DEFAULT_HORIZON_URL = "https://horizon.stellar.org"; +const DEFAULT_STELLAR_NETWORK: StellarNetworkName = "PUBLIC"; + +function readPublicEnv(name: string): string | undefined { + const value = process.env[name]; + if (typeof value !== "string") { + return undefined; + } + + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +/** + * Public (browser-safe) configuration. + * Values come from NEXT_PUBLIC_* env vars with safe defaults when unset. + */ +export function getPublicEnv() { + const networkRaw = readPublicEnv("NEXT_PUBLIC_STELLAR_NETWORK")?.toUpperCase(); + const network: StellarNetworkName = + networkRaw === "TESTNET" ? "TESTNET" : DEFAULT_STELLAR_NETWORK; + + return { + stellarNetwork: network, + horizonUrl: readPublicEnv("NEXT_PUBLIC_HORIZON_URL") ?? DEFAULT_HORIZON_URL, + sorobanRpcUrl: readPublicEnv("NEXT_PUBLIC_SOROBAN_RPC_URL"), + contractId: readPublicEnv("NEXT_PUBLIC_CONTRACT_ID"), + } as const; +} + +/** + * Returns true when a candidate string looks like a secret that must never + * appear in committed templates or NEXT_PUBLIC_* values. + */ +export function looksLikeSensitiveSecret(value: string): boolean { + const trimmed = value.trim(); + + if (!trimmed) { + return false; + } + + // Stellar secret keys + if (/^S[A-Z2-7]{55}$/.test(trimmed)) { + return true; + } + + // Ethereum-style private keys + if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) { + return true; + } + + // Common cloud API key prefixes + if (/^(sk-|sk_live_|sk_test_|AKIA)[A-Za-z0-9_-]{16,}$/.test(trimmed)) { + return true; + } + + return false; +} diff --git a/frontend/src/lib/stellar.ts b/frontend/src/lib/stellar.ts index 4de2816..5f8dbe7 100644 --- a/frontend/src/lib/stellar.ts +++ b/frontend/src/lib/stellar.ts @@ -3,8 +3,7 @@ */ import { createError, ErrorCodes, StandardError } from "@/lib/errors"; - -const HORIZON_URL = "https://horizon.stellar.org"; +import { getPublicEnv } from "@/lib/env"; interface AccountDetails { id: string; @@ -16,6 +15,10 @@ interface AccountDetails { }>; } +function getHorizonUrl() { + return getPublicEnv().horizonUrl.replace(/\/$/, ""); +} + /** * Fetches the account details for a given Stellar public key * @param publicKey - The Stellar public key @@ -25,7 +28,7 @@ export async function getAccountDetails( publicKey: string, ): Promise { try { - const response = await fetch(`${HORIZON_URL}/accounts/${publicKey}`); + const response = await fetch(`${getHorizonUrl()}/accounts/${publicKey}`); if (!response.ok) { if (response.status === 404) { throw createError(ErrorCodes.STELLAR_ACCOUNT_NOT_FOUND);