Skip to content

Make conversation IDs unguessable and scope chats to their shop#67

Open
n33levo wants to merge 2 commits into
mainfrom
chat-idor/scope-conversations-to-shop
Open

Make conversation IDs unguessable and scope chats to their shop#67
n33levo wants to merge 2 commits into
mainfrom
chat-idor/scope-conversations-to-shop

Conversation

@n33levo

@n33levo n33levo commented Jun 8, 2026

Copy link
Copy Markdown

What this fixes

This addresses the IDOR reported in Shopify/consumer-agent#1826 for the shop-chat-agent reference 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

  • New conversations now use crypto.randomUUID() instead of a timestamp.
  • History reads and chat continuation now require a shop ID and reject conversations that do not belong to that shop.
  • Stale/deleted conversation_id values on chat POST mint a fresh UUID so existing browser tabs can recover, while existing wrong-shop/null-shop conversations still return 404.
  • /chat handles CORS preflight through both route entry points before attempting to parse a chat request body.
  • Pre-stream /chat failures return JSON response headers instead of SSE headers.
  • Legacy conversations with no shopId are deleted by the migration because they cannot be safely scoped.
  • The migration also deletes CustomerToken and CustomerAccountUrls rows for those legacy conversations so customer auth data is not orphaned, then rebuilds Conversation with shopId non-null.
  • OAuth state no longer uses a deterministic value: it carries conversationId, shopId, and a random nonce so repeated auth attempts do not collide with existing CodeVerifier.state rows.
  • OAuth state decoding accepts the legacy hyphen-delimited production format during rollout.
  • Customer-token reads now require the request shopId and validate that the owning conversation belongs to that shop before returning a token.
  • Assistant/tool message persistence now carries shopId, and the DB helper refuses to save without a shop ID or to a conversation owned by another shop.
  • Added Node tests for the regression cases Marcelo called out: missing shop, legacy null shop, wrong shop, valid shop, UUID OAuth state parsing, unique OAuth state, JSON pre-stream responses, stale conversation IDs, and route-level missing-shop/preflight behavior.

Important security note

X-Shopify-Shop-Id is 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.
  • Syntax-checked touched JS/JSX/test files with node --check.
  • git diff --check.
  • SQLite migration smoke test confirmed legacy rows are deleted and Conversation.shopId is non-null.

@n33levo n33levo requested a review from marcelobarreto June 8, 2026 16:22

@marcelobarreto marcelobarreto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Review assisted by pair-review

Comment thread app/routes/chat.jsx
Comment thread app/routes/chat.jsx Outdated
Comment thread app/routes/chat.jsx Outdated
Comment thread app/routes/chat.jsx Outdated
Comment thread app/routes/chat.jsx Outdated
Comment thread app/routes/chat.jsx Outdated
Comment thread app/routes/chat.jsx
@n33levo n33levo force-pushed the chat-idor/scope-conversations-to-shop branch 2 times, most recently from 50cc7e3 to 335e2d8 Compare June 8, 2026 17:46
@n33levo

n33levo commented Jun 8, 2026

Copy link
Copy Markdown
Author

Thanks @marcelobarreto! you were right on these. I pushed a fix and rechecked the stack.

What changed on this PR:

  • Missing X-Shopify-Shop-Id now returns 400 before history lookup or new conversation creation.
  • Legacy/null-shop conversations no longer pass the guard (conversationBelongsToShop rejects missing shop, null shop, wrong shop).
  • The migration deletes legacy conversations that cannot be safely scoped.
  • UUID OAuth state is fixed: state now uses : and is decoded with lastIndexOf, so UUID hyphens are preserved.
  • Assistant/tool saves now pass shopId; createOrUpdateConversation also refuses to create a row without shopId.
  • PR description was updated to call the shop header defense-in-depth, not a true signed security boundary. App Proxy remains the durable follow-up.
  • Added tests in this PR for missing shop, null legacy shop, wrong shop, valid shop, and UUID OAuth-state parsing.

I also hardened the stacked PR (#68): auth.token-status now validates conversation/shop before checking CustomerToken, and the widget sends the shop header during token polling.

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.
@n33levo n33levo force-pushed the chat-idor/scope-conversations-to-shop branch from 335e2d8 to 030dddd Compare June 8, 2026 23:49
@n33levo

n33levo commented Jun 8, 2026

Copy link
Copy Markdown
Author

Follow-up audit after re-reading the stack end-to-end: I pushed 030dddd with three additional hardening fixes.

  • /chat now handles OPTIONS in both route entry points before parsing a chat request body, so browser preflight for X-Shopify-Shop-Id does not fall into the POST chat path.
  • The migration now deletes legacy CustomerToken and CustomerAccountUrls rows for deleted null-shop conversations, not just Message/Conversation.
  • createOrUpdateConversation now refuses saves with a missing shop ID and refuses to save to an existing conversation owned by another shop.

Re-verified on this PR: node --test 8/8, git diff --check, and syntax checks on touched JS/JSX/test files.

@marcelobarreto marcelobarreto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Review assisted by pair-review

Comment thread app/auth.server.js
Comment thread app/db.server.js
Comment thread app/routes/chat.jsx Outdated
Comment thread prisma/schema.prisma Outdated
Comment thread test/conversation-access.test.js

@n33levo n33levo left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Review assisted by pair-review

Comment thread app/auth.server.js Outdated
Comment thread app/auth.server.js
Comment thread app/routes/chat.jsx Outdated

@n33levo n33levo left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Review assisted by pair-review

@n33levo n33levo force-pushed the chat-idor/scope-conversations-to-shop branch from b50b09f to 8881405 Compare June 9, 2026 20:45
@n33levo n33levo requested a review from siddhantbajaj June 10, 2026 17:13
@semgrep-pr-bot

semgrep-pr-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

⚠️ Deprecated LLM Models Detected

This PR contains references to 1 deprecated model(s). Consider migrating to the recommended replacements.

⚠️ claude-sonnet-4-20250514 (anthropic)

  • Occurrences: 1
  • Severity: WARNING
  • Shopify Replacement: claude-sonnet-4-5
  • Hotswap Date: 2026-01-15

Locations:

  • app/services/config.server.js:9

Recommended Action: Replace with claude-sonnet-4-5 before 2026-01-15


📚 Resources

💡 This is an informational warning - it will not block your PR. However, please plan to migrate away from deprecated models before their shutdown dates.
💡 Please reach out to #help-proxy-shopify-ai if you have any questions.

🤖 Posted by Deprecated Models GitHub App

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