
I 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.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
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.
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.
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.
The ten critical sites. Each exposed a credential or dataset with unrestricted or administrative access.
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.
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.
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.
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.
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.
28 sites, including the feature-flag key below. Most were API keys that bill or authenticate as the owner.
The exposure of an LLM key is billing and quota abuse, plus whatever the key's account can reach.
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.
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:
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.
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.
We have sent notifications to over 50 sites, received replies from 8 and confirmed fixes with 4. We will
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。