.env Files — 6 Mistakes That Get Your Secrets on GitHub

Обновлено

Most .env leaks aren't from hackers — they're from developers who committed files before .gitignore was set up, shipped .env.example with live values, or let a framework quietly bundle server secrets into client JS. Here are the 6 mistakes that actually happen.

.env Files — 6 Mistakes That Get Your Secrets on GitHub 1
Реклама · УДАЛИТЬ?

GitGuardian’s 2023 State of Secrets Sprawl report found over 12 million secrets committed to public GitHub repositories. Most weren’t stolen — they were uploaded by developers who genuinely thought they’d handled it. These are the patterns responsible.

1. Adding .gitignore after the first commit

.gitignore только предотвращает добавление неотслеживаемых files from being staged. Once a file is tracked — even briefly — it’s in git history. If you created .env, ran git add . && git commit, then added .env к .gitignore afterward, the file is still in every commit that preceded that change.

Check whether it’s already in history:

git log --all -- .env

If that returns commits, the secrets are in history. Rotate credentials first. Then remove the file from history using git-filter-repo (the recommended replacement for git filter-branch):

pip install git-filter-repo
git filter-repo --path .env --invert-paths

Force-push to all remotes and notify teammates to reclone. The commits exist in every clone that was made before the purge — including any automated CI systems that checked out the repo.

2. Copying .env to .env.example without scrubbing values

The standard workflow: create .env with real values, then copy it to .env.example to show teammates what keys the project needs. The copy is where things go wrong.

cp .env .env.example copies everything — keys и values. And .env.example is supposed to be committed. That’s the entire point of it. Real values in .env.example go into the repo intentionally.

❌ What ends up in git:

DATABASE_URL=postgres://admin:supersecretpassword@prod-db.example.com/appdb
STRIPE_SECRET_KEY=sk_live_51AbcDefGhiJklMnopQrstUvwx...
JWT_SECRET=my-actual-production-jwt-secret

✅ What .env.example should look like:

DATABASE_URL=postgres://user:password@localhost:5432/appdb
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
JWT_SECRET=generate-a-random-secret-min-32-chars

Создавать .env.example with placeholder values first, commit it, then copy it to .env and fill in real credentials — not the other way around.

3. Logging process.env in error handlers

This one starts as a “quick debug” during an incident and never gets removed. Or it lives in generic error middleware that looks harmless.

// Classic debug line that makes it to production
console.log('Starting with config:', process.env);

// Generic error handler that dumps everything
app.use((err, req, res, next) => {
  logger.error({ config: process.env, error: err.message });
  res.status(500).json({ error: 'Internal server error' });
});

process.env at runtime contains every variable dotenv loaded, plus system variables. Passing the full object to a logger means it lands in your log aggregator, your error tracking service (Sentry, Datadog, Rollbar), and potentially in error notification emails or webhooks. Many of those services forward to third-party storage with their own access controls.

Log only the specific values you need for diagnosis:

logger.error({
  nodeEnv: process.env.NODE_ENV,
  appVersion: process.env.APP_VERSION,
  error: err.message,
  stack: err.stack
});

4. Baking secrets into Docker image layers

Two patterns that permanently embed secrets into Docker image history:

# Pattern 1: COPY bakes the entire .env into a layer
COPY .env .

# Pattern 2: ARG/ENV burns values into build metadata
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL

Even if you delete the file in a later layer (RUN rm .env), the value is still readable in the image history. Anyone with pull access to the image can run:

docker history --no-trunc your-image:tag

And recover the ARG values used at build time. Docker BuildKit secrets are the correct tool — they mount secrets during the build without writing them to any layer:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=db_url     DATABASE_URL=$(cat /run/secrets/db_url) ./setup.sh

For runtime config, inject environment variables at container start via docker run -e или environment: in Docker Compose referencing host env vars — never hardcoded values, never COPY‘d secret files.

5. Using weak placeholder secrets that ship to production

JWT_SECRET=secret, SESSION_SECRET=keyboard cat, APP_KEY=changeme, ENCRYPTION_KEY=1234567890abcdef. These start as dev placeholders and sometimes never get replaced. Attackers brute-forcing JWT signatures actively try these strings — they’re in wordlists specifically because they show up in GitHub search.

A JWT signed with HS256 and a weak secret can be cracked offline with tools like c-jwt-cracker. One intercepted valid token is enough to brute-force the secret and mint arbitrary tokens.

Proper secrets should be cryptographically random, minimum 32 bytes. Generate them before you need them — the Генератор секретов окружения on IO Tools will produce properly random values for common .env secrets (JWT keys, session secrets, API keys) without requiring any setup. Set them from the start; don’t use a placeholder and plan to “fix it before prod.”

6. Framework env var conventions exposing secrets to the client

Several popular frameworks use variable naming prefixes to determine client vs. server visibility. Getting this wrong ships secrets in the JavaScript bundle to every browser that loads your app — in plaintext.

  • Next.js: только NEXT_PUBLIC_-prefixed variables are bundled client-side. But server-side secrets leak when passed through getServerSideProps props — any value returned in props gets serialized into the page HTML and is readable in the source.
  • Vite: Variables prefixed VITE_ are bundled into client JS. Using VITE_DATABASE_URL “for convenience” is a mistake developers actually make.
  • Create React App: Все REACT_APP_ variables end up in the client bundle, without exception. There is no server-side CRA runtime — everything that loads goes to the browser.

Verify after build by searching the output directory for known secret values:

grep -r "sk_live_" ./dist
grep -r "sk_live_" ./.next/static

If any matches come back, those secrets are in every browser tab. Rotate them immediately and audit what else was bundled.

One habit worth building from the start

Before creating any file that will hold secrets, set up .gitignore first — not as an afterthought. The first commit in any new repo should be .gitignore и .env.example with placeholder values. The .gitignore Генератор will produce a complete, framework-specific ignore file in under a minute.

The six mistakes above are all preventable before any code is written. Rotation is the only fix once secrets are exposed — and that means rotating everywhere: the service provider, every environment that had a copy, and every system that might have cached the value in logs.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?