DEV Community

Cover image for The security gap between "it works locally" and "it's live"
doureios39
doureios39

Posted on

The security gap between "it works locally" and "it's live"

Most developers treat deployment as the finish line. Code works, tests pass, push to production, done. But there's a gap between "it works locally" and "it's live on the internet" where security quietly falls apart.

I built a pre-deployment scanner and over 100 developers have used it in the past few weeks. The same mistakes show up everywhere. Not sophisticated vulnerabilities - just things that got forgotten in the rush to ship.

Here are the six most common ones.

1. .env files served publicly**

This is the big one. Your .env file has database passwords, API keys, and secrets. Locally, it sits safely in your project root. In production, if your web server isn't configured to block it, anyone can visit yoursite.com/.env and read everything.

It happens more than you think. A recent study of hackathon repos found that 17% had leaked credentials. And those are just the ones committed to git - the deployed versions are often worse.

Fix: Make sure your server or hosting platform blocks requests to dotfiles. In Nginx, add:

location ~ /\. {
    deny all;
}
Enter fullscreen mode Exit fullscreen mode

Most frameworks handle this by default, but custom setups and VPS deploys often miss it.

2. Database files at non-standard paths

Static security scanners check for /backup.sql or /db.sqlite3. But real apps don't name their files that way. They use names like myapp.db, production_data.sqlite3, or the app name itself as the filename.

I recently caught a deployed app serving its entire database file at a URL that no static scanner would ever guess - because the filename was based on the project name, not a common default. The scan detected it in about 1.3 seconds using directory listing detection and dynamic filename checks.

This is the kind of thing that only gets caught if you actually probe the live deployment instead of checking a list of known paths.

Fix: Never store database files inside your web root. Keep them outside the directory your web server serves, or configure your server to block access to common database extensions:

location ~* \.(db|sqlite3|sqlite|sql|mdb)$ {
    deny all;
}
Enter fullscreen mode Exit fullscreen mode

3. Open database ports

Locally, your Postgres runs on port 5432 and nobody can reach it. On a VPS, that same port might be open to the internet. Same with Redis (6379), MongoDB (27017), and MySQL (3306).

An open database port with weak or default credentials is an invitation. Bots scan for these constantly - within minutes of spinning up a new server, you'll see connection attempts on common database ports.

Fix: Configure your firewall to only allow database connections from your application server, not from the public internet:

sudo ufw allow from your_app_ip to any port 5432
Enter fullscreen mode Exit fullscreen mode

Or bind the database to localhost only in its configuration file.

4. Missing security headers

Your app works fine without them. Nobody notices they're missing. But security headers like HSTS, Content-Security-Policy, and X-Frame-Options protect your users from real attacks - clickjacking, protocol downgrade attacks, XSS.

Most frameworks don't add these by default. You have to configure them yourself, and in the rush to ship, they're the first thing that gets skipped.

Fix: At minimum, add these headers:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'
Referrer-Policy: strict-origin-when-cross-origin
Enter fullscreen mode Exit fullscreen mode

Where you add them depends on your stack - Nginx config, middleware, or your framework's response headers.

5. .git directory exposed

If you deploy by cloning or pulling your git repo on the server, the .git directory might be accessible from the web. That means anyone can reconstruct your entire source code, see your commit history, and find secrets that were committed and later removed.

Fix: Either deploy without the .git directory (build artifacts only), or block access to it in your web server config:

location ~ /\.git {
    deny all;
}
Enter fullscreen mode Exit fullscreen mode

6. Debug endpoints and admin panels left open

Django's debug mode shows full stack traces with variable values. Express error handlers dump internal details. Admin panels at /admin sit behind nothing but a login form - or sometimes not even that.

I've seen deployed apps with /api/admin/users endpoints returning full user records with no authentication. Not because the developer forgot to add auth - because the AI-generated code never included it in the first place.

Locally, these are useful. In production, they're attack surfaces.

Fix: Always set DEBUG=False (Django), NODE_ENV=production (Express), or equivalent for your framework before deploying. Put admin panels behind VPN or IP restrictions, not just authentication.

The pattern

None of these are hard to fix. Each one takes five minutes. The problem is nobody checks. There's no step between "deploy" and "move on to the next feature" where someone looks at the live deployment from the outside and asks: is anything exposed that shouldn't be?

Code scanners check your source. Dependency scanners check your packages. But the live deployment - the thing your users actually interact with - gets deployed and forgotten.

What I built

This is why I built Preflyt. Paste a URL or run it from your terminal:

npx preflyt-check https://your-site.com
Enter fullscreen mode Exit fullscreen mode

30 seconds, no signup. Checks for exposed files, open ports, missing headers, and common misconfigurations. If you use AI coding agents, drop a skill file in your project and scans run automatically after every deploy.

Free for 3 scans at preflyt.dev.


What deployment mistakes have you run into? Drop them in the comments.

Top comments (0)