Skip to content

Secure /api/claude endpoint against key-burning abuse#3

Open
samirasadov28-code wants to merge 1 commit into
mainfrom
claude/secure-api-endpoint-TsoRV
Open

Secure /api/claude endpoint against key-burning abuse#3
samirasadov28-code wants to merge 1 commit into
mainfrom
claude/secure-api-endpoint-TsoRV

Conversation

@samirasadov28-code
Copy link
Copy Markdown
Owner

Summary

Closes the unauthenticated, wide-open /api/claude Netlify Edge Function flagged by the osmlab/awesome-openstreetmap maintainer (issue #183). Anyone could POST to it from any origin and burn the LLM API key with zero friction.

  • Origin allowlist — drops the Access-Control-Allow-Origin: * header and only accepts requests whose Origin or Referer is on StoryRoute's own domain(s); everything else gets 403.
  • Per-IP rate limits — 6 req/min sliding window + 120 req/day cap, returning 429 on exceed (best-effort in-memory, paired with the origin check).
  • Prompt size cap — 4,000 chars total across messages[], returning 413 otherwise, so a single abusive call can't drain huge token counts.
  • Version bumped to v2.3.1 across public/index.html (splash + JS header) and public/sw.js (cache name) per CLAUDE.md.

Note on the report

The endpoint actually proxies Groq (GROQ_API_KEY, api.groq.com) — only the path/filename is claude. The abuse risk is the same, and is now mitigated; the Groq key should be rotated in Netlify env vars since it may have been scraped while the endpoint was open.

If StoryRoute uses a custom domain beyond storyroute.app / storyroute.netlify.app, add it to ALLOWED_ORIGIN_SUFFIXES in netlify/functions/claude.js.

Test plan

  • Deploy to Netlify with GROQ_API_KEY still set, confirm the app (served from the allowlisted origin) still generates stories.
  • curl -X POST https://<site>/api/claude -d '{"messages":[{"role":"user","content":"hi"}]}' -H 'Content-Type: application/json' with no Origin → expect 403.
  • Same curl with -H 'Origin: https://evil.example' → expect 403.
  • Fire 10 rapid POSTs from the allowed origin → expect the 7th–10th to return 429.
  • POST a 5,000-char prompt → expect 413 Prompt too large.
  • Rotate GROQ_API_KEY in Netlify and verify the app keeps working with the new value.

https://claude.ai/code/session_012uCC14gY6NRHz9cowfjiag

- Replace CORS `*` with an origin allowlist and require Origin/Referer
  to match before accepting a request, blocking cross-site browser abuse.
- Add per-IP sliding-window (6/min) + daily (120/day) rate limits and
  a 4k-char prompt cap so a single abusive caller can't drain the key.
- Bump to v2.3.1 (index.html splash + JS header, sw.js cache name).
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for storyroute ready!

Name Link
🔨 Latest commit 2dc14ca
🔍 Latest deploy log https://app.netlify.com/projects/storyroute/deploys/69e23119d4119e0008fd7c8a
😎 Deploy Preview https://deploy-preview-3--storyroute.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

samirasadov28-code pushed a commit that referenced this pull request Apr 25, 2026
…(v2.7.0)

#2 Walk-to indicator
- bearingTo() + bearingArrow() compute compass direction to next landmark
- updateWalkIndicator() writes "↗ 180m" alongside the next-stop name in
  #next-info; called from prefetchStory() when nxtStory is set and from
  the GPS watchPosition handler so distance updates as the user walks

#3 Route summary panel before tour start
- After planRoute() fetches landmarks, showRouteSummary() renders an ordered
  stop list (sorted by routeIndexOf) with walk distance + estimated minutes
  between each pair; replaces the old immediate setMode('explore') jump
- Each stop has a ✕ button → removeRouteStop() adds to routeSkipped Set and
  re-renders; nearest() now skips routeSkipped landmarks in route mode
- "Start Tour · N stops" button calls startRouteTour() which hides the panel
  and calls startTour(); "← Edit route" goes back to the route input panel
- routeSkipped is cleared whenever a new route is planned

#4 Polyline-buffered landmark fetching
- sampleRouteEvery(intervalM) walks routeCoords accumulating haversine
  distance and emits a sample point every intervalM metres (plus the last
  point), replacing the old 8 evenly-indexed fixed samples
- planRoute() picks interval = radius/2.5 so fetch circles overlap: 1000m
  for walking (2500m radius), 3200m for driving (8000m), 8000m for long
  (20000m); fetchArea's tile-level dedup prevents redundant API calls

https://claude.ai/code/session_015rSh7JMd58DyLGhUDcsQWe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants