We Scanned 3 Months of Product Hunt Launches for Security Issues
We audited three months of Product Hunt launches and found 69 sites leaking real credentials or user data, from a live Stripe secret key to database admin keys. Plus security basics, dead sites and what votes predict.

squirrelscan is an open-source CLI for website QA. It's deterministic, with over 300 rules around reliability, SEO, security, performance and more.
Product Hunt is a forum for launching new apps and has become an increasingly popular method to launch vibe-coded and other apps.
We decided we would use a 3 month sample of Product Hunt launches to test squirrelscan.
The headline findings:
- Real credential and data exposures in public pages, from a password-hash dump to live payment, database and LLM keys: 69 sites, 51 of them Medium or worse, all confirmed manually.
- On average each Product Hunt launch day includes two sites with security issues.
- 71.3% of launches send no Content-Security-Policy header, and 92.0% publish no
security.txt. - A big launch day doesn't mean a safe site: the most-upvoted launches had the worst median security score.
- Size of companies predicted nothing. Vulnerability notices were sent to public companies, funded startups, small teams and solo developers.
- Responsibly reporting security issues is extremely difficult because of the lack of security contacts and responses to notices.
Method
Corpus. Product Hunt's daily leaderboards from 1 August to 27 October 2026: 72,356 launches over 88 days. We followed Product Hunt's own redirect to each product's website. GitHub repositories, app store and browser-extension listings, marketplace pages, sign-in walls, duplicates and spam were excluded.
Audits. squirrelscan CLI v0.0.138 with --level surface --max-pages 30 --probe passive, run on cloud sandboxes and a laptop. These were surface scans: up to 30 pages per site, no requests beyond the crawl, plain HTTP and no browser rendering. squirrelscan does a lot more than that, and the squirrel audit docs list them (browser rendering, deeper crawls, active security probes, etc.).
The vote cohort. Every launch was audited regardless of its votes: 17,745 scored sites.
Verification. Every exposure below was confirmed by hand against public bytes. Where a finding could not be confirmed without using the credential, we say so.
Limits. A passive, surface-level crawl of up to 30 pages, without JavaScript execution, finds what a visitor's browser would receive and nothing deeper. We make no claim about what a full or active scan would have found on these sites. It says nothing about how a site behaves behind login.
The leaks, ranked by severity
Every exposure that is still live is described anonymously: no names, domains or identifying details, and the code shapes below are synthetic. We rate severity by what an outsider could do with the exposed value according to the vendor's own documentation, not by how embarrassing it is.
69 sites leaked a real secret or user data, 51 of them Medium or worse.
Examples, by severity:
- Critical: a live Stripe secret key, Supabase
service_rolekeys, Elasticsearch superuser credentials, whole site-config dumps holding storage, OAuth, payment and SMTP secrets, and a dump of user records with password hashes. - High: LLM and AI API keys in browser code, SMTP credentials, OAuth client secrets, hardcoded admin credentials, Calendly personal access tokens and a feature-flag server key.
- Medium: Telegram bot tokens, Discord and Slack webhooks, image-host, OCR and blockchain API keys, and an Instagram long-lived token.
- Low: quota-limited or read-only keys (movie database, stock photo, map and weather APIs) and demo logins that reach the real backend.
The critical cases
The ten critical sites. Each exposed a credential or dataset with unrestricted or administrative access.
1. A Stripe live secret key in the browser bundle
A small, self-funded B2B SaaS for small businesses that received a handful of votes on launch day had their live Stripe key embedded in their source.
A REACT_APP_ env block in the production JavaScript bundle held both the publishable key (pk_live_…) and the secret key (sk_live_…, 107 characters) side by side, next to a reCAPTCHA site key.
// synthetic shape, not the site's code
const env = {
REACT_APP_STRIPE_PK: "pk_live_••••",
REACT_APP_STRIPE_SK: "sk_live_••••",
REACT_APP_RECAPTCHA_SITE_KEY: "6L••••",
};2 to 6. Five Supabase service_role keys
A small crypto and fintech company, a small AI developer-tools company, a solo-built B2B SaaS, an insurance claims app still at the demo stage, and one app whose operator we could not identify; each received a small number of votes.
Five sites shipped a service_role key to the browser. Four passed it straight to createClient(url, key) in the client bundle. One shipped it as serviceKey in an inline window config object, right next to the anon key. We read the role from the token's public claims (role: service_role, a Supabase issuer). The exp claims are about ten years out, in 2036.
// synthetic shape
const supabase = createClient("https://••••.supabase.co", "eyJhbGciOi…");
// or: window.__CONFIG__ = { anonKey: "eyJhbGciOi…", serviceKey: "eyJhbGciOi…" }Supabase's API keys docs say the secret key maps to the service_role Postgres role and "bypasses every Row Level Security policy you have", and "never put one in a browser, a shipped application, or source control". We did not test whether these keys are accepted from a browser.
7. Elasticsearch superuser credentials
A consumer AI app built by a solo developer with a small number of votes.
An axios request config in a client bundle sent search queries to the site's Elasticsearch backend with HTTP basic credentials for the built-in elastic user.
// synthetic shape
axios.post("https://search.••••/items/_search", query, {
auth: { username: "elastic", password: "••••" },
});Elastic's built-in users docs call elastic "a built-in superuser" that can "manage security and create roles with unlimited privileges", and say not to use it unless full cluster access is required. We did not test whether the cluster is reachable from the internet.
8. A whole server config serialised into the page
A consumer web tool whose operator we could not identify and who didn't respond to emails to support or sales.
An inline JSON settings object in the HTML carried Cloudflare R2 storage credentials (access key ID, secret access key and bucket), a Google OAuth client secret (GOCSPX-…), a payment provider's merchant secret, proxy credentials, SMTP settings and a Telegram bot token with its support chat ID.
<!-- synthetic shape -->
<script id="site-config" type="application/json">
{"storage":{"accessKeyId":"••••","secretAccessKey":"••••","bucket":"••••"},
"oauth":{"clientSecret":"GOCSPX-••••"},
"smtp":{"host":"••••","user":"••••","pass":"••••"},
"telegram":{"token":"••••","chatId":"••••"}}
</script>This is the client/server boundary failure in its purest form: the server's settings object went to the browser wholesale. Cloudflare's R2 API token docs describe the access key pair as credentials for S3-compatible access to buckets, with what it can do set by the token's permission level, which we cannot see. We have not verified the other vendors' scopes.
9. A VITE_ env block holding a company's backend
A small, self-funded fintech and trading-analysis company.
One production bundle held a Twilio account SID and auth token, a Shopify API secret (shpss_…), API secrets for two trading platforms, an email-sending API key, a market-data key and a backup service token, all as VITE_ variables.
// synthetic shape, after bundling
const env = { VITE_TWILIO_AUTH_TOKEN: "••••", VITE_SHOPIFY_SECRET: "shpss_••••", VITE_EMAIL_API_KEY: "••••" /* … */ };Twilio's security docs say the Auth Token "provides access to the REST API" and warn never to hard-code it in "browser Javascript". Vite's env docs say VITE_ variables "will be exposed in client-side source code" and "should not contain sensitive information such as API keys". We have not verified the scopes of the other vendors' secrets.
10. Whole user records in a page's props
A small, self-funded community platform that received a handful of votes.
A community platform's blog page embedded full user documents for seven author accounts in its __NEXT_DATA__ JSON, at props.pageProps.data[n].author. The hashes repeated 52 times across posts. Values below are masked by us; the structure is as served.
"author": {
"_id": "6•••••••••••••••••••••••",
"password": "$2b$10$•••••••••••••••••••••••••••••••••••••••••••••••••••••",
"is_admin": false,
"is_password": true,
"verified": true,
"otp": null,
"otp_expiry_time": null,
"_sessions": ["…"],
"phone": { "number": "…", "prefix": "…" },
"whatsapp_number": { "number": "…", "prefix": "…" },
"google": { "id": "…", "email": "…", "picture": "…" },
"facebook": { "email": "…", "id": "…", "picture": "…" },
"registration_ip": null,
"last_ip": null,
"full_name": "••••••••••••",
"public_email": "•••••@•••••.com"
}Mechanism: a server-side data fetch returned whole user documents into the page props, and Next.js serialises page props into the HTML so the client can hydrate. Nothing was hacked. The query returned the author object, and the framework did what it does with props. See the client/server boundary below.
The site never replied to our disclosure and is still live six weeks later. Not everyone was like that: a large public company fixed its leaked server key and emailed us to say so, and one SaaS removed and rotated its exposed CMS token within two hours of our email.
High severity: the patterns
28 sites, including the feature-flag key below. Most were API keys that bill or authenticate as the owner.
| Pattern | Sites | Detail |
|---|---|---|
| LLM and AI API keys in browser code | 15 | OpenAI, Anthropic, xAI, Groq, DeepSeek, Moonshot (Kimi), OpenRouter (3 sites), an LLM router, Hugging Face, Tavily, ElevenLabs. One inline script carried five providers' keys in request headers. One Groq site used the SDK's dangerouslyAllowBrowser: true. OpenAI project keys came partly via REACT_APP_. |
| Hardcoded admin credentials | 2 | Plaintext admin passwords in an admin-portal script; a hardcoded admin Bearer token that the client attaches when it decides the user is an admin (one of two established software vendors) |
| OAuth client secrets for password-grant logins | 2 | The other established software vendor is one of these |
| SMTP usernames and passwords | 2 | In client code |
| Facebook app secret | 1 | In an HTML data attribute emitted by a WordPress plugin |
| Calendly personal access tokens | 2 | Full account tokens, not embed tokens |
| Feature-flag SDK server key | 1 | A server secret key in the homepage config of a large public company; fixed after our report |
| Other | 3 | A personal login prefilled in form state, long-lived API JWTs sent from the browser to an email or subscriber API (2), an image-processing API key. Some sites appear in more than one row. |
The exposure of an LLM key is billing and quota abuse, plus whatever the key's account can reach.
Medium and low severity
Medium (13 sites): Telegram bot tokens (2), Discord webhooks, a Slack webhook, a search API secret key, an image-host key, a CMS preview secret, keys for the site's own APIs, blockchain API keys (2), an Instagram long-lived token, an OCR key and a backend user token baked into a no-code app's API connector. Low (18): quota-limited or read-only keys and demo logins, such as movie-database read tokens, stock-photo, map and weather keys, and demo accounts that log into the real backend. These are the ones where the key is meant to be public or the damage is a quota.
The client/server boundary
How they shipped. At least 11 of the sites leaked through a VITE_, REACT_APP_ or NEXT_PUBLIC_ env block in the bundle. Most of the rest were keys hardcoded into client code or SDKs initialised in the browser. A handful were server data serialised into the HTML. All three are the same problem.
Most of the leaks in this post share a root cause: code or data that was meant to stay on the server crossed into what the browser receives. This is not specific to one framework, but Next.js and React Server Components make it easy to do by accident, and the official guidance describes four routes.
Page props are public. In the Pages Router, the getServerSideProps docs say that props "can be viewed on the client as part of the initial HTML" so the page can hydrate, and warn not to pass anything sensitive. That is the user-records case above: a whole database document returned as props.
Server Components pass props to Client Components. The App Router data security guide has an example of a Server Component that queries a user row and passes it to a Client Component, with the comment that this "exposes all the fields in userData to the client". The fix it recommends is to sanitise the data and return only the public fields.
Env prefixes inline secrets. Per the Next.js environment variables docs, a NEXT_PUBLIC_ variable is inlined at build time into JavaScript sent to the browser. Vite does the same for VITE_: its env docs say VITE_ variables "will be exposed in client-side source code" and "should not contain sensitive information such as API keys". Create React App's docs say REACT_APP_ variables are "embedded during the build time" and "anyone can view them by inspecting your app's files". Every prefix means the same thing: this goes into the bundle. The Stripe, Twilio and several LLM keys in the table above were in exactly such blocks.
Server SDKs and keys in browser code. The same guide says Client Components "must not access privileged data or server-only modules". A server key passed into a client-side config object, as in the feature-flag case above, defeats that by hand.
What the framework documents as mitigations:
- Return only what the UI needs. The guide recommends a Data Access Layer that returns minimal Data Transfer Objects (DTOs), and says only that layer should read
process.env. - Mark modules with
server-only. Importing aserver-onlymodule from client code causes a build error, per the same guide. - Taint APIs. React's
experimental_taintObjectReferenceandexperimental_taintUniqueValueblock a tainted object or value from being passed to a Client Component. Both are experimental, and React's pages say not to rely on tainting alone: cloning an object or deriving a value produces something untainted. Next.js calls it an additional layer of protection.
LLM SDKs show the same thing from the other side. The OpenAI Node SDK README disables browser use by default "to avoid exposing your secret API credentials" and makes you set dangerouslyAllowBrowser to opt in. A flag with that name is the SDK telling you the key is about to become public.
None of these is a scanner substitute, which is the point of auditing the output rather than the source: the HTML a site serves is the only place all four routes show up together.
An observation about who is shipping this
Now that everyone is building with LLMs, our read is that both amateur developers and experienced developers at large public companies are letting security issues leak into their code. The data is consistent with that: the leaks came from a large public company and from tiny indie launches alike, and dozens of other sites made the same kinds of mistake. We did not measure LLM usage, so this is an observation, not a finding. What we can say is that the mistake is not confined to beginners.
Notifications
We have sent notifications to over 50 sites, received replies from 8 and confirmed fixes with 4. We will update the report at a later date with fix rates, but at the moment it is abysmal. 92% of audited Product Hunt launches had no security.txt or a security contact that could be easily found. Not one of the sites with a medium or higher severity issue published a security contact via security.txt or a security@ email or any other obvious path, further hampering disclosure and fixes. 18 of the sites were contacted via generic support email addresses (with no disclosure in the initial emails).
We are working to further notify every affected owner before or alongside publication, and we are not naming anyone with a live exposure (although the results are relatively easy to replicate).
The disclosure paradox
There is a contradiction in how this works. A company that answers and fixes can be named, and credited. A company that ignores the report cannot be named, because naming it points attackers at a live hole. Silence buys anonymity, and the owners who respond are the ones who end up on the record.
The security community's usual answer is a fixed deadline. Google Project Zero's disclosure policy gives a vendor 90 days after notification to ship a fix, and publishes details at the end of that period if there is no patch. We may come back in 90 days and disclose the complete details and names for all vulnerabilities, but it feels inadequate.
Security basics across the corpus
Leaks were the dramatic part. The everyday gaps were everywhere. Figures are shares of the scored sites.
| Check | Sites |
|---|---|
| No Content-Security-Policy header | 71.3% (a further 22.7% send a weak one) |
No security.txt | 92.0% |
No clickjacking protection (X-Frame-Options or CSP frame-ancestors) | 61.9% |
| Missing or weak HSTS | 40.6% |
Cookies missing Secure or HttpOnly | 13.4% |
| Mixed content | 0.4% |
| Secrets rule fired | 4.1% |
What the secrets rule flagged
There were a total of 2,179 leaked secrets findings across 852 sites. 195 were ruled out as public by design while there were some false positives (and will contribute to improving squirrelscan), and we couldn't validate 202 findings in live and current site data when checking again for various reasons, largely because of the delay between audits and manual verification.
Does a big launch mean a healthy site?
We expected a weak link, maybe a negative one: launches that win votes are polished on the surface, and surface is not where these checks look. We compared votes with audit results on the complete cohort (17 August to 15 September, 17,745 scored sites, see Method).
Show the numbers
| Votes | Sites | Security | SEO | Performance | Overall |
|---|---|---|---|---|---|
| 1 | 10,485 | 80 | 57 | 51 | 51 |
| 2 to 4 | 5,943 | 83 | 62 | 55 | 54 |
| 5 to 9 | 530 | 83 | 60 | 54 | 52 |
| 10 to 24 | 254 | 81 | 64 | 48 | 53.5 |
| 25 to 49 | 46 | 86 | 65.5 | 46 | 54.5 |
| 50 to 99 | 154 | 85 | 63.5 | 50.5 | 54 |
| 100 to 249 | 238 | 84 | 56 | 48 | 50 |
| 250+ | 94 | 63 | 57 | 46 | 49 |
Across the cohort, votes and scores are unrelated: the Pearson and Spearman correlations between votes and the overall, security, SEO and performance scores all sit within 0.08 of zero. A big launch day doesn't come with a better site.
The exception is at the top. The most-upvoted launches, 250 votes and up, have the worst median security score: 63, against 80 to 86 for every other band. It is a small band (94 sites), so treat it as a flag, not a law.
Featured launches (1,612 sites) have a median overall score of 52 and security 85. Non-featured launches in the cohort (17,489) score 52 and 81. Featuring buys a few points of security and nothing else. Also: 59% of launches in the cohort got exactly one vote.
Votes measure the pitch, not the site: the launches that drew the most attention had the weakest security. They do predict one thing, whether the site is still online two months later (see the dead-site table).
Launched, then gone
Some launch sites didn't make it to an audit at all. A site counts as dead when it failed two checks at least three hours apart. Only 34 of the 1,831 sites that failed the first check came back on the second, so 98% of failures held.
Show the numbers
| Weeks since launch | Sites | Dead | Share |
|---|---|---|---|
| 0 | 232 | 1 | 0.4% |
| 1 | 363 | 4 | 1.1% |
| 2 | 527 | 10 | 1.9% |
| 3 | 409 | 5 | 1.2% |
| 4 | 415 | 4 | 1.0% |
| 5 | 439 | 14 | 3.2% |
| 6 | 311 | 7 | 2.3% |
| 7 | 280 | 9 | 3.2% |
| 8 | 375 | 12 | 3.2% |
| 9 | 273 | 7 | 2.6% |
| 10 | 307 | 7 | 2.3% |
| 11 | 361 | 17 | 4.7% |
| 12 | 270 | 6 | 2.2% |
Checked 2026-10-28. Dead means the site failed its latest check; of the sites that failed once, 98% failed again at least three hours later. The line is a 15-day average over launches with 5 or more votes, the vote level we could resolve for every launch day.
The chart uses launches with 5 or more votes only (4,562 sites), the vote level we resolved for every launch day. About 1% are dead in the first month, rising to about 3% by two to three months. In the chart, dead means the latest check failed, and 98% of failures were confirmed by the second check.
The complete 17 August to 15 September cohort covers every launch, all 6 to 10 weeks old (19,886 sites). Here the share dead depends heavily on votes:
| Votes | Sites | Dead |
|---|---|---|
| 1 | 11,967 | 6.8% |
| 2 to 4 | 6,531 | 4.8% |
| 5 to 9 | 565 | 3.2% |
| 10 to 49 | 318 | 3.5% |
| 50 or more | 504 | 1.6% |
| All launches | 19,886 | 5.9% |
One-vote launches are dead at more than four times the rate of launches with 50 or more votes (6.8% against 1.6%).
Of the 1,166 confirmed dead sites, the causes were:
| Cause | Sites |
|---|---|
| Server error or 404 at the homepage | 407 |
| Not responding (timeouts, refused connections, TLS failures) | 374 |
| Domain no longer resolves | 233 |
| Hosting deployment deleted or paused | 138 |
| Parked, for sale or suspended | 14 |
The hosting cases include Vercel's DEPLOYMENT_NOT_FOUND and DEPLOYMENT_DISABLED errors (the latter comes with a 402 and an x-vercel-error header).
Check yours before you launch
Every finding above came from the same command you can run today:
squirrel audit https://your-launch.comIt crawls your site, runs over 300 audit rules across security, performance, accessibility, SEO and agent experience, and hands your coding agent a list of exact fixes. Run it the week before launch day, not the week after.
Acknowledgements
Thank you to Anthropic for allowing us to do this security research with their models.
Sources checked 28 October 2026: Supabase API keys, Elasticsearch built-in users, Cloudflare R2 API tokens, Twilio security, Vite env variables, Create React App env variables, Next.js data security guide, getServerSideProps, Next.js environment variables, React taintObjectReference, React taintUniqueValue, OpenAI Node SDK, Google Project Zero disclosure policy.