The swap file, the SQLite db, and the Redis dump your app left in the web root

You SSH into the box to fix one value in .env, you open it in vim, the connection drops mid-edit. Vim was holding an unsaved copy in a swap file, and that swap file, .env.swp, is still sitting next to the real one. It has the full plaintext of what you were editing. Nobody deleted it because nobody knew it was there.

This is the shape of the whole problem. Not a file you wrote and forgot, but a file the running app or your editor wrote for you, on disk, in a directory the web server happens to serve. The swap file vim left after a crash. The SQLite database the framework defaults to. The Redis snapshot a cron job wrote at 3am. They're runtime state, the live working set of a running system, and when they land in the web root they're a download away from a stranger.

These three are caught the same way a keystore or backup archive is: by their magic bytes, the fixed signature in the first few bytes that says what the file actually is. That matters here more than anywhere, because these files don't have one name. A swap file can be .env.swp or .settings.py.swp or a .swo rollover. A SQLite db is whatever the framework called it. A Redis dump renames trivially. You can't catch them by name, so the check reads the bytes.

A swap file is the full plaintext of whatever you were editing

A vim swap file isn't a fragment. It's vim's recovery copy of the entire buffer, so it holds the complete contents of the file you had open, in plaintext, regardless of how that file is normally protected. Edit your .env over SSH and the swap is your secrets. Edit wp-config.php and the swap is your database credentials and your salts. Edit a settings module and the swap is whatever lives in it, hidden routes, internal hostnames, tokens.

It gets left behind two ways. An editor crashed or the SSH session dropped before vim cleaned up, so the .swp outlives the edit. Or a deploy was too broad, a COPY . . or an rsync with no exclude, and it swept the swap file into the image alongside the real file. Either way the server treats .env.swp as a static file like any other. It returns the bytes.

The magic bytes are b0VIM (62 30 56 49 4D 20) at the very start. So a swap for any file, under any of the conventional names, is recognized for what it is.

request
GET /.env.swp HTTP/1.1 Host: yoursite.com
response
HTTP/1.1 200 OK
Content-Type: application/octet-stream
b0VIM 8.2 ... <- Vim swap magic bytes
(the full plaintext of .env: DB_PASSWORD=..., API keys, JWT_SECRET=...)
The swap is the whole buffer. Editing .env over SSH leaves .env.swp, and that file is your secrets in plaintext.

A SQLite or Redis file is the whole datastore, downloaded and read offline

The swap leaks one file. These two leak the data.

A SQLite database is a single file that is the entire database: every table, every row. Frameworks reach for it by default and the default name lands in a predictable spot, database.sqlite for Laravel, db.sqlite3 for Django, a plain database.db for plenty of others. If that file is under the web root and the server serves it, a stranger downloads the whole thing in one GET. Then it's local on their machine. They open it with any SQLite client and read every table at their leisure: your users, your password hashes, your session and API tokens. No rate limit, no lockout, nothing in your logs, because all the reading happens offline.

A Redis RDB dump is the same idea for your in-memory store. Redis periodically snapshots itself to disk as a .rdb file, dump.rdb by default, sometimes redis-dump.rdb or backup.rdb from a cron job. That snapshot is everything Redis was holding: active sessions, auth and refresh tokens, cached PII, whatever you put in there. Download it and the attacker is holding live sessions and valid tokens, the keys to walk straight into accounts.

The magic bytes are why renaming doesn't help. SQLite starts with the 16-byte string SQLite format 3\0. Redis starts with REDIS (52 45 44 49 53) followed by four ASCII version digits, and that version-digit check is what separates a real RDB header from a file that merely happens to begin with the letters REDIS. The check confirms the signature, not the name.

$ yoursite.com
$ curl -s yoursite.com/database.sqlite | head -c 16
SQLite format 3\0
$ # download the whole file, open it locally, read every table
users: email, password_hash, api_token
One download and the database is on the attacker's laptop. They read every table offline, no rate limit and nothing in your logs.

Where they actually show up

$ scan yoursite.com
probing yoursite.com from outside, no credentials...
/.env.swpVim swap, full plaintext of .env
/db.sqlite3SQLite DB: users + password hashes + tokens
/dump.rdbRedis dump: sessions + auth tokens
3 exposures visible to anyone. None required a login.
The swap for an edit, the framework's default db file, the cron's Redis snapshot. Each one is a full read in a single request.

Keep runtime state off the web host

None of these belong in a directory the web server can reach. The SQLite file lives outside the document root, readable only by the app process, the same place a leaked .env should live. Redis snapshots write to a data directory the web server never serves, off the public path entirely. And configure your editor and your deploy so the swap file never travels: an *.swp exclude in vim's directory setting keeps swaps in a scratch dir, and an rsync/.dockerignore exclude keeps them out of the image.

If any of these was reachable, even briefly, the contents are compromised. A file that was downloadable was downloaded. Rotate every secret the swap file held, rotate the credentials and tokens in the database, and invalidate every session and token that was in the Redis dump. Deleting the file now doesn't un-leak what already left.

Reading it from outside

Whether your box answers an anonymous request for /.env.swp or /dump.rdb is something a stranger settles by fetching the path and looking at the first few bytes, which is exactly what SurfaceCheckr does. It requests the canonical and host-derived names, reads only the leading bytes of the response, and confirms a hit on the magic-byte signature: a real b0VIM swap header, a real SQLite format 3\0, a real REDIS header with valid version digits. Never just a 200, never a matching filename. It doesn't pull the whole file down, it confirms the file is what it claims to be and stops. That gate is what keeps the check honest, a soft-404 that 200s everything doesn't trip it, and a renamed dump doesn't slip past it. It's passive, it reads only what the public can already see, and it's not a pentest, no login and no attack payload.

Run a scan, and if any of these answered, treat the contents as already stolen and start rotating.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.