Skip to content

fix(auth): bind CoinPay sign-in to the CoinPay sub, not the unverified email - #655

Merged
ralyodio merged 1 commit into
masterfrom
fix/coinpay-sub-binding
Oct 27, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/coinpay-sub-binding

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

CoinPay userinfo always returns email_verified=false (CoinPay never verifies addresses), but the CoinPay callback adopted any existing ugig account with the same email (createUser -> email_exists -> listUsers by email -> magic link). Anyone could register a CoinPay account with a victim's address and sign into their ugig account.

Changes

  • New src/lib/coinpay-login.ts: the account is chosen only through oauth_identities (coinpay, sub). Unbound sub + email already on ugig -> refused with coinpay_email_in_use (login page explains: sign in normally, connect CoinPay from Settings -> Connections). Unbound sub + unused email -> new account bound to the sub.
  • The session magic link is generated for the bound account's own auth email, never the CoinPay email.
  • Connect mode: the coinpay_oauth_state cookie is unsigned JSON, so a rewritten userId could attach the caller's CoinPay to someone else's profile (then log in as them). The callback now requires the signed-in user to match.
  • Tests: src/lib/coinpay-login.test.ts, src/app/api/callback/oauth/route.connect.test.ts.

No migration needed: existing CoinPay users are already bound by sub in oauth_identities.

🤖 Generated with Claude Code

…ied email

CoinPay never verifies emails, so a CoinPay account registered with a
victim's address used to log into the victim's ugig account (createUser
email_exists -> listUsers lookup by email -> magic link for that email).

- Login resolves the account only via oauth_identities (provider, sub).
- An unbound CoinPay login whose email is already on ugig is refused
  (coinpay_email_in_use): sign in normally, then connect CoinPay from
  Settings -> Connections.
- The session magic link is generated for the bound account's own email,
  not the CoinPay-supplied one (a changed CoinPay email could point at
  another account).
- Connect mode: the state cookie is unsigned JSON, so its userId must equal
  the currently signed-in user or the link is refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

60 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 12 | LOW: 47

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:80
MEDIUM js-open-redirect src/app/checkout/CheckoutClient.tsx:77
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:102
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:114
MEDIUM js-unescaped-html-sink src/app/page.tsx:70
MEDIUM js-unescaped-html-sink src/app/pricing/page.tsx:84
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:138
MEDIUM js-open-redirect src/components/gigs/BuyNowButton.tsx:106
MEDIUM js-unescaped-html-sink src/components/layout/PitFooter.tsx:25
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/agent-register/route.test.ts:27
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:172
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:196
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:215
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:228
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:267
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:297
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:329
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:346
LOW secret-generic-credential src/app/api/gigs/[id]/buy/route.test.ts:48
LOW secret-generic-credential src/app/api/gigs/[id]/buy/route.test.ts:99
LOW secret-generic-credential src/app/api/gigs/[id]/buy/route.test.ts:158
LOW secret-generic-credential src/app/api/orders/[id]/release/route.test.ts:18
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191

…and 10 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 81f1e16 into master Oct 27, 2026
6 checks passed
@ralyodio
ralyodio deleted the fix/coinpay-sub-binding branch October 27, 2026 03:38
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