Skip to content

fix(api): reject another org's segment, topic and template ids on POST /v1/batch - #221

Merged
stewartjarod merged 1 commit into
mainfrom
fix/api-batch-segment-org-scope
Oct 28, 2026
Merged

stewartjarod merged 1 commit into
mainfrom
fix/api-batch-segment-org-scope

Conversation

@stewartjarod

Copy link
Copy Markdown
Contributor

Summary

POST /v1/batch (the route behind wraps broadcasts create) stored whatever segmentId the caller sent. An org-1 key created a broadcast with org 2's segment id and got a 200. This breaks invariant 4 (scope every lookup by organizationId).

 POST /v1/batch
+  segment  = findSegment(segmentId,   authContext.organizationId)  -> 404 if missing
+  topic    = findTopic(topicId,       authContext.organizationId)  -> 404 if missing
+  template = findTemplate(templateId, authContext.organizationId)  -> 404 if missing
   countRecipients
   insert batch_send
   enqueue

topicId and templateId had the same gap. Ids are checked before anything is counted, written or enqueued, and the 404 matches the segments routes, so the CLI reports NOT_FOUND.

Evidence

  • Before: org-A key + org-B segmentId -> expected 200 to be 404 (3 mocked tests), and the same on the real DB (rejects another org's segmentId and writes no broadcast). Found by running the CLI against the real API.
    After: all pass. batch-idor + batch.integration 16 passed; 40 related files / 266 tests pass; typecheck and biome clean.

Send time: a foreign segmentId already stored resolves to nobody in getContactsChunk/countBroadcastRecipients (both org-scoped), so it sends zero and cannot reach another org's contacts. The raw-HTML fallback when a template lookup is empty was not traced to the end.

Merge Danger

Door: two-way

Every id that is present is now validated, even when audienceType does not use it, so a stale id on an "all" broadcast now 404s instead of being stored.

Blast Radius: batch API clients

Existing bad rows are not cleaned up. Find them with:
select id, organization_id, segment_id from batch_send bs where segment_id is not null and not exists (select 1 from segment s where s.id = bs.segment_id and s.organization_id = bs.organization_id);

🤖 Generated with Claude Code

https://claude.ai/code/session_01GxJAQNUP4FKZHL1yzPDsGd

…POST /v1/batch

The batch route stored segmentId, topicId and templateId from the body
without checking they belong to the caller's organization. An org-1 key
could create a broadcast pointing at org 2's segment; the recipient count
came back 0 and the row was inserted anyway.

Each id is now resolved through an org-scoped lookup before anything is
counted or written. An unknown or foreign id returns 404 with
{ error: "Segment|Topic|Template not found" }, matching the segments
routes so the CLI maps it to NOT_FOUND.

Send-time paths already scope these lookups by organization, so existing
rows with a foreign id send to nobody rather than leaking contacts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GxJAQNUP4FKZHL1yzPDsGd
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
wraps-web Skipped Skipped Oct 28, 2026 6:32pm UTC
wraps-website Skipped Skipped Oct 28, 2026 6:32pm UTC

Request Review

@vercel
vercel Bot temporarily deployed to Preview – wraps-website October 28, 2026 18:31 Inactive
@vercel
vercel Bot temporarily deployed to Preview – wraps-web October 28, 2026 18:31 Inactive
@stewartjarod
stewartjarod merged commit 1d36f9f into main Oct 28, 2026
17 of 20 checks passed
@stewartjarod
stewartjarod deleted the fix/api-batch-segment-org-scope branch October 28, 2026 19:45

This branch was previously deployed

2 inactive deployments
Preview – wraps-website — 95fdd26f Deployed Oct 28, 2026 by vercel[bot]
Preview – wraps-web — 95fdd26f Deployed Oct 28, 2026 by vercel[bot]
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.

1 participant