Skip to content

fix(api): never trust the CoinPay email on Sign in with CoinPay - #171

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

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

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

CoinPay never verifies email addresses (userinfo reports email_verified=false), so the email it hands back proves nothing.

Before: the CoinPay callback stored that email in the unique users.email column. It also set email_verified = info.email_verified ?? !!info.email, which counts an omitted flag as verified. Account resolution was already by sub, so there was no takeover. But:

  • a CoinPay account could squat an address before its real owner signed up, and the owner's signup then returned 409
  • a CoinPay login whose email matched an existing account crashed with a unique-violation 500

After (services/api/src/coinpay-login.ts):

  • The account bound to the CoinPay sub is the only one a CoinPay login reaches.
  • A new CoinPay account never takes an email another account already holds (it is created without one), and it is never marked verified.
  • A password signup can reclaim an address held only by an unverified, password-less CoinPay account. The new account still verifies by email.

Tests: coinpay-login.test.ts covers victim-email login, another sub, verified-flag handling and the unique race.

🤖 Generated with Claude Code

CoinPay does not verify email addresses, so its userinfo email proves
nothing. The callback stored it in the unique users.email column and set
email_verified from `info.email_verified ?? !!info.email`, which treated an
omitted flag as verified. A CoinPay account could squat an address before its
owner signed up (their signup then 409'd), and a CoinPay login whose email
matched an existing account failed with a unique-violation 500.

Now the account is resolved by sub only; a new CoinPay account never takes
an email another account holds and is never marked verified; a password
signup can reclaim an address held only by an unverified, password-less
CoinPay account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ThreatCrush Security Scan

50 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 27 | LOW: 21

Severity Rule Location
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:303
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:437
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:156
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:34
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:57
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:246
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:275
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:345
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:306
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:78
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:166
MEDIUM redos-nested-quantifier apps/desktop/extensions/ai-sidebar/w3bs.js:17
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:571
MEDIUM js-unescaped-html-sink apps/web/public/app.js:29
MEDIUM js-unescaped-html-sink apps/web/public/dns.js:54
MEDIUM sh-remote-script-execution apps/web/public/install.sh:182
MEDIUM sh-remote-script-execution apps/web/public/install.sh:422
MEDIUM sh-remote-script-execution apps/web/public/install.sh:440
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:96
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:168
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:125
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:68
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:73
MEDIUM js-unescaped-html-sink services/api/src/profile.ts:59
MEDIUM sql-template-interpolation services/api/src/store/db.ts:116
MEDIUM js-dynamic-code-execution services/api/src/store/scanner.ts:44
MEDIUM sh-predictable-temp-path start.sh:28
LOW js-dynamic-code-execution apps/desktop/extensions/ai-sidebar/pit-proxy.test.js:8
LOW js-dynamic-code-execution packages/agent-runtime/src/analyze/form-script.test.ts:6
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:26
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:49
LOW js-dynamic-code-execution packages/browser-core/src/automation/extract-script.test.ts:6
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:16
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:45
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:57
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:80
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:99
LOW js-dynamic-code-execution packages/browser-core/src/automation/snapshot-script.test.ts:11
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:62
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:101
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:47
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:170
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:216
LOW js-dynamic-code-execution packages/sdk/src/mcp/automate.test.ts:261
LOW secret-generic-api-key packages/storage/src/config.ts:51
LOW secret-generic-credential packages/storage/src/config.ts:51
LOW js-dynamic-code-execution services/api/src/store/scanner.test.ts:32
LOW js-hardcoded-crypto-key services/api/src/store/signing.test.ts:24

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 0d4b14c into main Oct 27, 2026
8 checks passed
@ralyodio
ralyodio deleted the fix/coinpay-sub-binding branch October 27, 2026 03:41
sh1pt-actions-fleet Bot added a commit that referenced this pull request Oct 29, 2026
… in settings (#173)

The CoinPay client (cp_56ff186e…) never sent PKCE, so it had to stay in
CoinPay's legacy mode. Now, via @profullstack/coinpay-onboard/oauth 0.3.1:

- start: PKCE S256 + state + nonce, kept in one httpOnly cookie scoped to
  /api/auth/coinpay; scopes exactly openid profile email (the old request
  asked for did + wallet:read, which the client was never registered for,
  so CoinPay dropped them; sign-in only uses the sub).
- callback: constant-time state check; code exchange with the verifier and
  the client secret (client_secret_post only, not both methods); id_token
  iss/aud/exp/nonce checked and the userinfo sub matched.
- Sub binding unchanged (#171): a CoinPay login never claims an account by
  email.
- New: Settings -> Connect CoinPay (signed in only) / Disconnect (refused
  when the account has no password). /api/auth/me reports both.

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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