Your staging environment is on the internet with no authentication, your preview deployments generate a new public URL for every pull request, and both can reach things production can reach.
Nobody exposed them deliberately. Staging was public because that was the default when it was set up, and preview URLs are public because sharing them is the point.
This post is about staging as an attack surface, separately from what data it holds. For whoever set up the preview pipeline.
Why it is attractive
An attacker choosing between your production environment and your staging environment will often prefer staging, for reasons that have nothing to do with the data in it.
It is less hardened. Rate limiting relaxed, error messages verbose, debug endpoints enabled, a permissive cross-origin policy, and a security header set that was configured for convenience.
It runs code that is not in production yet. A feature mid-development, behind no flag, with its authorisation not yet finished. You are hosting your unfinished work publicly.
It is not monitored. Alerting is configured for production. An intrusion in staging generates nothing.
It frequently has real credentials. A staging environment that integrates with production services, a third-party sandbox that is not actually a sandbox, or a cloud role that was copied from production because narrowing it was fiddly.
It may be inside the network. If staging sits in the same network as production and can reach internal services, it is a public foothold with internal reachability, which is the most valuable thing on this list.
Preview deployments multiply it
A URL per pull request means dozens of live environments, each running unreviewed code, each with whatever environment variables the pipeline injects, each reachable by anyone who has the address.
Three properties make them worse than staging:
They are numerous and short-lived, so nobody inventories them, and an old one left running is invisible. They run unmerged code, including code from a fork if your pipeline builds those. And they often get the same secrets as staging, because a single environment variable set is simplest.
If your preview pipeline builds pull requests from forks and injects secrets, that is a straightforward path for an outside contributor to obtain them, and it is the same shape as the self-hosted runner problem.
What to do
Require authentication. The single most effective change and the simplest: put staging and previews behind your identity-aware proxy or the platform's access control, so reaching them requires being someone. Most hosting platforms support this now and it is off by default.
Keep them out of search engines. noindex, a robots file, and ideally authentication rather than relying on either. Staging environments turning up in search results is a recurring source of embarrassment and occasionally of disclosure.
Separate the network. Staging should not reach production services, and if it needs to, that path should be as narrow and as deliberate as any other cross-boundary access.
Give them their own credentials. Distinct keys for every third party, so a leak from a preview environment does not touch production. Scoped, rotatable, and revocable independently.
Never inject secrets into a fork build. Build the fork's code with nothing, and if the preview needs a backend, point it at a shared mock rather than a credentialed one.
Expire them. A preview that outlives its pull request by a week should be destroyed automatically. The cleanup job is the difference between a handful of environments and a long tail nobody can enumerate.
Find what you have
# What preview and staging hostnames exist, per certificate transparency?
curl -s "https://crt.sh/?q=%25.yourcompany.com&output=json" \
| jq -r '.[].name_value' | tr '[:upper:]' '[:lower:]' | sort -u \
| grep -E 'staging|preview|dev|test|pr-|deploy' | head -30
# Which of them answer without authentication?
while IFS= read -r h; do
printf "%-45s " "$h"
curl -s -o /dev/null -m 8 -w "%{http_code}\n" "https://$h/"
done < hosts.txt
# Is staging indexed?
# Search for your staging hostname in a search engine.
The certificate transparency query is the useful one, because every environment with a certificate is publicly listed whether or not you remember it. It is the same query worth running for subdomain takeover, and it answers two questions at once.
The concession
Authentication on previews has a genuine cost: the point of a preview URL is to send it to a designer, a product manager or a customer for feedback, and requiring them to be in your identity provider defeats that for external reviewers.
The workable version is authentication by default with a deliberate, expiring exception: a shareable link with a token and a lifetime, for the specific preview that needs outside review. That keeps the default closed and the common sharing case possible, which is better than the usual arrangement where everything is open because one workflow needed it to be.
The implication
Staging is the environment with production's reachability, less hardening, unfinished code and no monitoring, and it is public because nobody chose otherwise.
Run the certificate transparency query against your own domain and try the hostnames it returns. Whatever answers without asking who you are is part of your attack surface.