Surfaced during the supply-chain hardening audit in #68.
Location: src/app/api/subscribe/route.ts
Issue: Public POST endpoint with no rate limit, no Origin/Referer check, and no body-size cap. Every call instantiates a Resend client and hits the paid Resend API. Specifically:
- Trivially DoS-able from any HTTP client.
- Burns through the Resend monthly contact-creation quota.
- Cross-origin POSTs are not rejected (browser CORS doesn't apply to non-browser clients).
alreadySubscribed: true in the success response reveals whether an email is already in the list — minor enumeration concern for a public mailing list, but worth closing.
Proposed fix:
- Reject requests whose
Origin (or Referer, on browsers that strip Origin) doesn't match the deployed site origin. Always return 403 for cross-origin POSTs.
- Add an IP-keyed rate limit. Two reasonable options:
- Simpler: in-memory bucket on Fluid Compute (per-instance, decent enough at our traffic).
- Stronger: Vercel BotID (which also catches automation) or an Upstash Redis bucket via the Vercel Marketplace.
- Cap request body size (1KB is plenty for
{email: "..."}).
- Return the same success payload regardless of
alreadySubscribed — drop the field.
Threat model context: the parent PR (#68) closes the dependency-side supply-chain gaps. This issue covers the application-side attack surface that consumes those dependencies.
Surfaced during the supply-chain hardening audit in #68.
Location: src/app/api/subscribe/route.ts
Issue: Public POST endpoint with no rate limit, no Origin/Referer check, and no body-size cap. Every call instantiates a Resend client and hits the paid Resend API. Specifically:
alreadySubscribed: truein the success response reveals whether an email is already in the list — minor enumeration concern for a public mailing list, but worth closing.Proposed fix:
Origin(orReferer, on browsers that strip Origin) doesn't match the deployed site origin. Always return 403 for cross-origin POSTs.{email: "..."}).alreadySubscribed— drop the field.Threat model context: the parent PR (#68) closes the dependency-side supply-chain gaps. This issue covers the application-side attack surface that consumes those dependencies.