NAKED DATABASES ON THE PUBLIC INTERNET

Every few months Shodan publishes a number for how many MongoDB instances are open on the public internet. The number stays large. AI-generated apps make it larger because the developer asks the AI to set up a database, the AI sets up a database, nobody asks who can reach it.

The scenario referenced below runs on gapbench.vibe-eval.com — a public security benchmark we operate.

The bug that’s older than the cloud

In 2017, MongoDB shipped a default config that bound the daemon to localhost. Before that, it bound to 0.0.0.0. For about a decade, every default Mongo installation was reachable from the internet. Attackers ran scans across the entire IPv4 space, found tens of thousands of open instances, and ransomed most of them. Mongo eventually changed the default. Redis followed a similar arc. Postgres’s default is more conservative but the operator can still bind it to 0.0.0.0 with a single config line.

The AI-codegen era brings this back, not because the defaults regressed, but because the path of least resistance for “set up a database in production” frequently routes around the default. A developer can’t connect from their machine. The AI suggests opening the port. The developer pastes the command. The database is now reachable. The check that should have happened — “wait, anyone can reach it now, not just me” — doesn’t happen because the AI didn’t surface that as a consequence.

What we actually find

We sweep the apex domain plus a few common subdomains (db, database, postgres, mysql, redis, cache, mongo, es, elastic, search) for the standard database ports. The hit rate is non-zero on real customer infrastructure. The hit rate on AI-generated infra specifically is higher than on hand-rolled infra, in our experience, because hand-rolled infra was set up once by someone who at least nominally knew what they were doing, while AI-codegen-shaped infra is often set up incrementally by someone who is asking the AI for help.

The four scenarios we have on gapbench cover the headline cases:

  • naked-postgres — port 5432 open, weak password or default credentials.
  • redis-open — port 6379 open, no auth (Redis’s default for years).
  • mongo-open — port 27017 open, no auth required because the operator forgot to enable it.
  • elasticsearch-open — port 9200 open, public search cluster, often with sensitive data indexed.

There’s also firebase-rules-open at /site/firebase-rules-open/ for the Firebase Realtime DB equivalent — rules so permissive any caller can read or write the entire tree.

The Kubernetes / cloud-control variant

A close cousin: kube-dashboard-open at /site/kube-dashboard-open/. The Kubernetes dashboard, exposed without auth, gives an attacker control of the cluster. From cluster control, you reach every database and every service in the cluster, often with their internal credentials available as Kubernetes secrets. We list this with the database scenarios because the impact is “reach every internal service” — same outcome, different attack path.

A specific incident

Anonymized. A small AI-product team had built a feature that needed Postgres to do full-text search on user-generated content. Supabase’s hosted Postgres works but their plan didn’t include the level of search performance the team needed, so they spun up their own Postgres on a $40/mo VPS. AI codegen wrote the install script. The script set listen_addresses = '*' because the team’s app server was on a different VPS and needed to connect.

What the script didn’t do: configure pg_hba.conf to restrict by IP, set up a security group, or change the default postgres user password. The default password was something predictable from the AI-suggested setup script. Three weeks after launch, the team’s monitoring caught a 6 AM CPU spike. They logged in to find the database had been read end-to-end — slow query log showed it — and then ransomed. A note in a new table: pay 0.1 BTC to restore. They didn’t pay; they had backups; they recovered. Two days of customer-support load and a postmortem. The bill could have been worse.

We see this exact shape often enough that we run a port sweep against customer apex domains as part of the standard scan. The hit rate is low (most teams don’t do this) but the impact when we hit is uniformly catastrophic. The fix is always the same: bind to localhost or to a private VPC, never expose the database publicly, never use the AI’s “make it work” suggestion if it relaxes network rules.

What “exposed” actually means

Different databases have different default behaviors and different exposure modes. Worth knowing the specifics:

  • Postgres. Default listen_addresses = 'localhost'. The exposure happens when someone changes it to '*' or '0.0.0.0'. Auth is on by default but uses trust for local connections in some default configs, which becomes dangerous when local turns into network.
  • Redis. Historical default was no auth. Modern (6+) versions ship with protected-mode yes which refuses connections from non-loopback unless a password is set or bind is configured. Exposure happens when protected-mode no or bind 0.0.0.0 is set without requirepass. Redis can also execute Lua scripts, which means an unauthenticated Redis often becomes RCE.
  • MongoDB. Default since 2017 is bindIp: 127.0.0.1 with no auth. Exposure happens when both are changed. mongod --auth is the flag that enables auth; it’s not on by default.
  • Elasticsearch. Default since 8.x is auth-on. Older versions (and many self-hosted setups) ran with auth off. Exposure path is identical to others — bind to public, no auth.
  • MySQL/MariaDB. Default bind-address = 127.0.0.1. Exposure when changed. AI codegen has been observed setting bind-address = 0.0.0.0 for “remote development access” without setting up a firewall.

How attackers find these

Shodan, Censys, and ZoomEye continuously scan IPv4 for open services. Anything reachable on a standard port shows up in their indices within a day. Searching for port:5432 returns hundreds of thousands of Postgres instances; the same for 6379 (Redis), 27017 (Mongo), 9200 (Elasticsearch). Most are private (auth required), but a non-trivial fraction respond to anonymous connection.

Beyond the public scanners, attackers run their own. Targeted attacks pick a victim domain, enumerate subdomains via DNS / certificate transparency, resolve each to its IP, and scan the IP for database ports. The technique is decades old and effective.

The defense isn’t “they won’t find me.” The defense is “they’ll find me and fail.” Which means auth, which means network ACLs, which means private networking.

Wrong fix vs right fix

# WRONG: bind to public, set a strong password
listen_addresses = '*'
password_encryption = scram-sha-256
# ALTER USER postgres WITH PASSWORD 'long-strong-password';
# Strong password helps but the database is still discoverable.
# Brute-force is not the only attack — known CVEs against Postgres protocol
# can sometimes bypass auth. Don't rely on auth alone.
# RIGHT: bind private, allow from app server, IP-restrict at firewall
listen_addresses = 'localhost,10.0.1.5'  # localhost + private IP only
# pg_hba.conf:
host all postgres 10.0.1.0/24 scram-sha-256
# Cloud security group: allow 5432 only from app server's private IP
# Strong password is the third layer, not the first.

Cross-platform notes

The exposure issue scales with how easily the platform makes it to skip the network controls.

  • Self-hosted (any cloud, manual setup). Highest risk. Defaults vary; AI codegen frequently relaxes them.
  • Managed (Supabase, RDS, Cloud SQL, MongoDB Atlas). Medium risk. The defaults are private, but most platforms have a “make it public” toggle. AI codegen sometimes flips it for “easier development connection.”
  • Edge / serverless DBs (Neon, PlanetScale, Turso). Lowest risk for exposure (they’re public-by-design, gated by API keys), but the keys leak through the same pathways the Supabase service-role key leaks. See The Supabase service-role key in your frontend bundle.

How we detect it

The detection is a port sweep plus a protocol probe. We open a TCP connection on the port, send the protocol handshake (SELECT 1 for Postgres, INFO for Redis, isMaster for Mongo, GET / for Elasticsearch), and observe the response. If we get back a banner indicating the database accepted the connection without auth — which most of them happily provide — we have a finding.

This is by definition runtime detection. The bug doesn’t live in source code. It lives at the network boundary. A static scanner reading your repo won’t find it. Even a CI lint can’t catch it, because the bind config might be in a Terraform file the lint doesn’t read, or in a cloud-console setting that isn’t in any file at all.

The vibe-code-scanner runs the database probe as part of its standard sweep when you give it a domain.

Fix

The fix is unambiguous: don’t bind to 0.0.0.0 unless you’ve thought hard about it.

For self-hosted databases: bind to localhost or to the internal cluster network only. If you need cross-host access, put the database on a VPC and only expose it to the application subnet. Cloud security groups, not the database’s own auth, are the load-bearing control here.

For ports exposed deliberately (rare, but happens): require authentication. Strong, unique credentials, rotated. Network ACLs that allow only the IPs that need to connect. TLS on the connection.

For managed databases: don’t expose them publicly. Supabase, Neon, PlanetScale, RDS — all have settings to put the database on a private network. Use the private network. The default for the AI to suggest “use the public connection string for simplicity” is the bug — overrule the AI here.

If you’ve been running with a public database for any length of time, assume it’s been read. Rotate every credential, audit data for tampering, and consider whether the data was sensitive enough to warrant a disclosure to users. The detection is cheap; the cleanup after the fact is not.

CWE / OWASP

  • CWE-306 — Missing Authentication for Critical Function
  • CWE-732 — Incorrect Permission Assignment for Critical Resource
  • CWE-284 — Improper Access Control
  • OWASP Top 10 — A05:2021 Security Misconfiguration

Reproduce it yourself

COMMON QUESTIONS

01
How does a database end up on the public internet?
Most often by accident. A developer spins up a Postgres container with default settings. The container binds to 0.0.0.0. The cloud provider's firewall has an 'allow all' default for some plans or for the developer's account. The database is reachable. Authentication is configured but with the default credentials, or with a weak password, or — for Redis and Mongo specifically — not configured at all because those products historically shipped with no-auth defaults.
Q&A
02
Why does AI codegen make this worse?
Because the AI is helpful in the same direction every time: get the database working. When the developer's app can't connect, the AI's fix is to relax network rules, not tighten them. We see AI-suggested commands like 'open port 5432 to 0.0.0.0/0' or 'set bind_ip = 0.0.0.0' as quick fixes for connection issues, and they ship to production.
Q&A
03
Don't managed services like Supabase prevent this?
Managed services prevent the worst version. Supabase doesn't expose Postgres directly; it puts PostgREST in front and enforces RLS. RDS and Cloud SQL have private-by-default networking on most setups. The exposure happens when developers spin up their own database — for cost, for control, for a feature the managed product doesn't have — and the AI helps them do it without the security defaults the managed product would have applied.
Q&A
04
What's the actual impact?
Total. An open database with no auth means an attacker reads every row, writes every row, deletes everything, or replaces it with a ransom note. We have seen Mongo instances ransomed multiple times — attackers script the takeover, drop the data, leave a note demanding bitcoin to restore. Open Redis is often used as a pivot to read session tokens or as a foothold (because Redis can run scripts, depending on configuration).
Q&A
05
Where can I see this on a real URL?
https://gapbench.vibe-eval.com/site/naked-postgres/, https://gapbench.vibe-eval.com/site/redis-open/, https://gapbench.vibe-eval.com/site/mongo-open/, https://gapbench.vibe-eval.com/site/elasticsearch-open/. Each is the corresponding database exposed to the internet without authentication on a known-vulnerable port.
Q&A
06
What CWE does this map to?
CWE-306 (Missing Authentication for Critical Function), CWE-732 (Incorrect Permission Assignment for Critical Resource), CWE-284 (Improper Access Control). OWASP A05:2021 (Security Misconfiguration).
Q&A

SCAN YOUR INFRASTRUCTURE

We probe your apex domain and likely subdomains for exposed database ports.

RUN THE SCAN