Make conversation IDs unguessable and scope chats to their shop#67
Make conversation IDs unguessable and scope chats to their shop#67n33levo wants to merge 2 commits into
Conversation
marcelobarreto
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
50cc7e3 to
335e2d8
Compare
|
Thanks @marcelobarreto! you were right on these. I pushed a fix and rechecked the stack. What changed on this PR:
I also hardened the stacked PR (#68): Verified:
|
…shop New conversations now get a random UUID instead of a millisecond timestamp, and every read/continue checks that the conversation belongs to the shop making the request. Together this closes the IDOR where you could read or resume someone else's chat by guessing the ID.
335e2d8 to
030dddd
Compare
|
Follow-up audit after re-reading the stack end-to-end: I pushed
Re-verified on this PR: |
marcelobarreto
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
n33levo
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
n33levo
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
b50b09f to
8881405
Compare
|
What this fixes
This addresses the IDOR reported in Shopify/consumer-agent#1826 for the
shop-chat-agentreference app.Before this PR, new conversations used
Date.now().toString()as the ID and the history/continue paths accepted any ID they were given. That made timestamp-style conversation IDs enumerable.What changed
crypto.randomUUID()instead of a timestamp.conversation_idvalues on chat POST mint a fresh UUID so existing browser tabs can recover, while existing wrong-shop/null-shop conversations still return 404./chathandles CORS preflight through both route entry points before attempting to parse a chat request body./chatfailures return JSON response headers instead of SSE headers.shopIdare deleted by the migration because they cannot be safely scoped.CustomerTokenandCustomerAccountUrlsrows for those legacy conversations so customer auth data is not orphaned, then rebuildsConversationwithshopIdnon-null.conversationId,shopId, and a random nonce so repeated auth attempts do not collide with existingCodeVerifier.staterows.shopIdand validate that the owning conversation belongs to that shop before returning a token.shopId, and the DB helper refuses to save without a shop ID or to a conversation owned by another shop.Important security note
X-Shopify-Shop-Idis still sent by the storefront client, so it is defense-in-depth, not a cryptographic proof of shop identity. The real protection this PR adds is that conversation IDs are server-created and unguessable. A durable customer/shop identity boundary should come from routing chat through a Shopify App Proxy and reading signed proxy parameters server-side.Verification
node --test— 16 passing, 3 Vite-backed route tests skipped locally because dependencies are not installed in this checkout.node --check.git diff --check.Conversation.shopIdis non-null.