Skip to content

Threat Model

This page is the canonical threat model. It lists what envGo defends against, the exact mechanism used, and — just as importantly — the limits of each defence.

Threat model

ThreatMitigationWhere it lives
Inspect Element / DevToolsSecrets never enter the browser. JavaScript only ever holds placeholder stringsserver.serveEnvJS, proxy.inject
Network tab sniffingThe browser talks to your own origin; the key is injected and the request is forwarded server-sideproxy.Handler.ServeHTTP
Cross-tab / XSS request forgerySession token plus Host plus Origin validation on /proxyserver.authorized
DNS rebindingHost header must match the bound address, localhost, or the bound hostserver.hostAllowed
SSRF via arbitrary targetsProxy is disabled when --allow is empty; otherwise only allowlisted hostsproxy.validateTarget
Abuse of fixed routesPer-route vars allow-set, per-route rate limit, optional bearer authgateway.inject, ratelimit.Limiter
Upstream secret leakscrub_response redacts known secret values from responseslogger.Redact
LAN / network exposureDefaults to binding 127.0.0.1 only; --qr is the one opt-in that widens it to 0.0.0.0main.go
TLS downgradeMinVersion is TLS 1.2 when using --tlsmain.go
PHP code execution30-second execution timeout per requestserver.servePHP
Accidental secret loggingEvery log line passes through a redacting loggerlogger.Logger.Redact
Secrets committed to gitenvgo init writes a .gitignore covering .envmain.go doInit
Config file disclosure over HTTPenvgo.routes.json, routes.json are always blocked from static serving, and dotfiles are never servedserver.serveStatic
Tampered self-updateenvgo run update refuses to install a download whose SHA256 is not proven to match the release manifestverifyChecksum, verifyDownload

Defences in detail

Browser isolation

The browser receives variable names, never values. /__env.js fetches /__envgo_dashboard/data, reads the vars array, and compares it against element IDs on the page. Only booleans are stored (window.EnvLoaded[name] = true).

Three-guard handshake on /proxy

Local mode rejects a proxy call unless all three checks pass:

  1. Host header matches a known address (anti DNS-rebinding)
  2. Origin header is absent or same-origin (anti cross-tab)
  3. X-EnvGo-Token equals the per-process session token, compared with subtle.ConstantTimeCompare

The token is 256 bits from crypto/rand, generated once per process start and served only by /__envgo_token. It has no expiry — it is valid for the life of the process, so restarting envGo rotates it.

Deny by default

  • No --allow → the proxy refuses every target
  • No default_rate_limit and no rate_limit → unlimited (this is the one place envGo does not deny by default; you must opt in to limiting)
  • No matching route → 404
  • Placeholder not in the route’s vars → hard 400, never a silent drop

Redaction

The logger is taught every secret value loaded from .env (excluding HOST and PORT). Every log line, the dashboard’s request history, and scrubbed response bodies have occurrences replaced with [REDACTED]. Longest values are replaced first so overlapping secrets are handled deterministically.

Self-update integrity

envgo run update is the only part of envGo that fetches an executable from the network, so it is the only part that can be attacked by tampering with a download. It refuses to install anything it cannot prove is the released binary:

  1. The platform binary is downloaded to a temporary file next to the running executable, so the final os.Rename happens on the same filesystem and is atomic. There is no window where a half-written binary sits at the target path.
  2. The SHA256 of that download is computed and compared against the release’s SHA256SUMS manifest.
  3. Only a proven match prints checksum OK and lets the install continue.
  4. The downloaded binary is then executed with -h as a smoke test, and only then swapped in.

The gate covers the fallback path too. When the GitHub API is unreachable, envgo still downloads directly from /releases/latest/download/, and that download goes through the same checksum check — an earlier version skipped verification entirely on that path.

A digest mismatch always aborts and cannot be overridden. The bytes you received are not the bytes that were published, and only the release author can explain that.

Being unable to verify is also a failure. If the manifest cannot be fetched, cannot be read, or does not list your platform’s asset, the update stops. These outcomes are all reported as “unverified” rather than treated as success, because an attacker who can substitute a binary can equally serve a manifest that omits it. The startup -h check is not a substitute: any binary can answer -h.

The escape hatch for a network that cannot reach the manifest at all — a corporate proxy, or an air-gapped mirror — is an explicit opt-out:

Terminal window
envgo run update --skip-checksum # or -k

It prints a warning and installs an unverified binary. Use it only when you control the mirror and have verified it yourself.

Known limitations

Be aware of these before trusting envGo with a high-value secret:

  • --qr deliberately widens the default exposure. Without it envGo binds 127.0.0.1 and nothing on the network can reach it. --qr binds 0.0.0.0 instead, so any device on the same Wi-Fi can load your static files and read variable names from the dashboard. The /proxy guards above still apply, and secret values are still never sent to the browser — but treat --qr as public-facing for the files it serves, and do not use it on an untrusted network. See Sharing with your phone.
  • Streaming responses are not scrubbed. SSE/streaming passthrough skips redaction entirely to avoid buffering. See Configuration.
  • The dashboard exposes variable names. Variable names and request metadata are readable by anyone who can reach the dashboard. It is enabled by default in local mode and opt-in in public mode. Do not enable it on a publicly reachable instance.
  • /__env.js depends on the dashboard data endpoint. Because it reads /__envgo_dashboard/data to learn the variable names, the helper only works where that endpoint is enabled.
  • Rate limiting is per-process, per-IP. Multiple instances multiply the effective limit, and behind a proxy without trust_proxy: true every request shares a single bucket.
  • A failed or missing PHP interpreter falls through to serving the source. If php is not installed, or a script exits non-zero or times out, envGo serves the .php file as a static download. Add PHP files only when a PHP interpreter is present. See PHP Support.
  • The session token is not a user authentication system. It stops a random cross-origin page from using your proxy. It does not authenticate users, and any page served from your own origin can obtain it.
  • Static files are served as-is. Anything in your web root — except the config files and dotfiles — is public, including a .env that happens to sit in the served directory. Do not serve the directory that holds your .env.
  • --tls uses a self-signed certificate. Browsers will warn. Use a reverse proxy with real certificates for anything public.
  • The self-updater trusts GitHub’s CDN, not a signature. The checksum proves the download matches the manifest that was served next to it; it does not protect against a GitHub account or CDN compromise that serves both a substituted binary and a substituted manifest. For a high-assurance setup, download the release yourself, verify it out of band, and replace the binary by hand instead of using envgo run update.

Security checklist for production

  • .env is in .gitignore and is not inside the served web root
  • --allow is set (local mode) or every route uses an explicit vars allow-set
  • default_rate_limit or per-route rate_limit is set — empty means unlimited
  • Running behind Caddy/nginx for TLS termination
  • envGo bound to 127.0.0.1, not 0.0.0.0 — and --qr not passed
  • trust_proxy: true set only when actually behind a proxy you control
  • Dashboard disabled (no --dashboard) in public mode
  • scrub_response enabled if upstreams might echo secrets
  • .env.example contains placeholder values only
  • Secrets rotated if they were ever committed or logged
  • --skip-checksum is not passed to envgo run update (it installs an unverified binary)

Reporting a vulnerability

Please report suspected vulnerabilities privately rather than in a public issue, and include reproduction steps plus a description of the impact.

Next steps