PromptSpend is a static site with no accounts and no login. Nothing a visitor types or
pastes into the calculator is sent anywhere, which removes most of the usual attack surface. The pages
load from two remote origins: https://launchnest.io, for one badge image
(img-src only), and https://static.cloudflareinsights.com, the Cloudflare Web
Analytics beacon (script-src), which reports only to the site's own
/cdn-cgi/rum, and reports a page's address without its query string or
fragment. Where the alerts API is configured, Turnstile's
https://challenges.cloudflare.com is admitted too.
There are two servers, both Cloudflare Workers, and only one of them holds anything:
- The opt-in price-alerts API in
worker/, on a D1 database holding email subscribers and push subscriptions. Small and deliberately boring, but it is a backend that stores personal data and can send mail, so it is squarely in scope. - The public pricing API in
api/, servingpromptspend.dev. It is stateless, keyless and read-only — it holds no data and has no accounts — but it re-serves prices to callers who cannot see the catalog behind it, which puts it inside the blast radius of the first item below.
Two more surfaces re-serve those prices without being servers at all: the MCP
server (mcp/) and the VS Code extension (vscode/). Neither
stores anything and neither accepts input from anyone but its own user, but a
wrong price reaching either is the same class of bug as a wrong price on the
site, and reports about them belong here.
The deployed site is built from main. There are no release branches and no
back-ported fixes: main is the supported version.
Either route works, whichever you prefer:
- security@promptspend.com
- GitHub's private vulnerability reporting — Security → Report a vulnerability on the repository
Please do not open a public issue with details in it.
Expect an acknowledgement within a few days. This is a small project maintained in spare time, so please allow a reasonable window before disclosing publicly.
- Wrong prices, and anything that could make them wrong. The pipeline publishes third-party data as fact. A way to get a bad number past the sanity rules, the schema validation or the review gate is the most serious class of bug this project has, even though it is not a "vulnerability" in the usual sense.
- Anything that could transmit a pasted prompt. Prompt text is the only
sensitive data the page ever holds. It is never sent anywhere. The Content
Security Policy is generated at build time (
vite.config.ts) and opensconnect-srconly for the alerts API origin and Turnstile'schallenges.cloudflare.com, so a compromised dependency has nowhere of its own to send it. - The alerts API. Specifically: forging or replaying a
/v1/notifycall; using a confirm, unsubscribe or preferences token for a purpose or a subscriber it was not issued for; subscribing an address somebody does not control; reading another subscriber's address or followed models; getting the worker to make a request to a host that is not a real push service; and any way to make it send mail beyond the rate limit and Turnstile check. Details and rationale are in docs/ALERTS.md. - Supply chain: a dependency, a GitHub Action, or the LiteLLM/OpenRouter feeds being used to reach the deployed site. Actions are pinned to commit SHAs; the pricing feeds are treated as untrusted input and cannot publish without passing validation.
- Export safety: the CSV export escapes and formula-neutralises every field
(
src/lib/engine/csv.ts). A way past that is in scope.
- Denial of service against GitHub Pages.
- Missing headers that GitHub Pages does not allow a static site to set. The
CSP and referrer policy are declared in
<meta>tags for that reason; if you know a way to set real headers there, that is a welcome issue rather than a vulnerability report. - Findings that depend on an attacker already controlling the visitor's browser or machine.
There is no cookie. The website (not the apps, extension or API) counts visits
with Cloudflare Web Analytics: a cookieless beacon, injected by Cloudflare, that
reports page address, referrer, country and timings to the site's own
/cdn-cgi/rum. The Content Security Policy admits its script origin
(static.cloudflareinsights.com) but keeps connect-src closed to it, so no
script on the page can post to a remote collector. Pasted prompt text stays in the tab: it is
tokenised locally, is deliberately excluded from the shareable URL, and is held
in a bounded in-memory cache that is cleared when the scenario is reset and
discarded when the tab closes. localStorage holds only interface choices:
theme, accent and background, and whether the welcome and apps banners were
dismissed.
If — and only if — you subscribe to price alerts, the alerts database holds:
- Browser push: the opaque endpoint URL the browser issued and its two encryption keys, plus the models you follow. Nothing that identifies a person.
- Email: your address, the models you follow, your cadence, and the date you subscribed. Consent is recorded as a salted hash of the IP, never the address itself. No name, no opens, no clicks. Cloudflare hosts the database and sends the mail; no other company receives the record.
Unsubscribing deletes the row and the follow list rather than flagging them. Addresses that never confirm are deleted within a week.