GitHub Security Advisory: GHSA-6g38-8j4p-j3pr
Package: github.com/nhost/nhost (GO)
Severity: CRITICAL (CVSS 0.0)
CWE: CWE-287: Improper Authentication
Vulnerable versions: < 0.0.0[PII_REDACTED]2436-ec8dab3f2cf4
Patched in: 0.0.0[PII_REDACTED]2436-ec8dab3f2cf4

Summary: Nhost Vulnerable to Account Takeover via OAuth Email Verification Bypass

Details:
## Summary

Nhost automatically links an incoming OAuth identity to an existing Nhost account when the email addresses match. This is only safe when the email has been **verified by the OAuth provider**. Nhost's controller trusts a `profile.EmailVerified` boolean that is set by each provider adapter.

The vulnerability is that several provider adapters **do not correctly populate this field** they either silently drop a `verified` field the provider API actually returns (Discord), or they fall back to accepting unconfirmed emails and marking them as verified (Bitbucket). Two Microsoft providers (AzureAD, EntraID) derive the email from non-ownership-proving fields like the user principal name, then mark it verified.

The result is that an attacker can present an email they don't own to Nhost, have the OAuth identity merged into the victim's account, and receive a full authenticated session.

## Root Cause

In `services/auth/go/controller/sign_in_id_token.go`, `providerFlowSignIn()` links a new provider identity to an existing account by email match with no verification guard:

```go
// sign_in_id_token.go:267-296
func (ctrl *Controller) providerFlowSignIn(
    ctx context.Context,
    user sql.AuthUser,
    providerFound bool,
    provider string,
    providerUserID string,
    logger *slog.Logger,
) (*api.Session, *APIError) {
    if !providerFound {
        // Links attacker's provider identity to the victim's account.
        // profile.EmailVerified is NEVER checked here.
        ctrl.wf.InsertUserProvider(ctx, user.ID, provider, providerUserID, logger)
    }
    // Issues a full session  to the attacker.
    session, _ := ctrl.wf.NewSession(ctx, user, nil, logger)
    return session, nil
}
```

The controller places full trust in whatever `profile.EmailVerified` the adapter returned. The vulnerabilities below show how that trust is violated.

## Correct Implementation (For Reference)

### GitHub: `providers/github.go`

GitHub fetches `/user/emails` and reads the `verified` boolean per entry. `selectEmail()` picks only verified emails. The result is correctly mapped:

```go
selected := selectEmail(emails)   // only selects verified: true entries

return oidc.Profile{
    Email:         selected.Email,
    EmailVerified: selected.Verified,  // real boolean from GitHub API
    ...
}
```

## Vulnerable Providers

### 1. Discord: `providers/discord.go`

**The Discord `GET /users/@me` API returns a `verified` boolean field.** Per Discord's official documentation and example User Object:

```json
{
  "id": "[PII_REDACTED]4678912",
  "username": "Nelly",
  "email": "nelly@discord.com",
  "verified": true
}
```

The Nhost struct is **missing this field**. Go's JSON decoder silently discards it:

```go
type discordUserProfile struct {
    ID            string `json:"id"`
    Username      string `json:"username"`
    Discriminator string `json:"discriminator"`
    Email         string `json:"email"`
    Locale        string `json:"locale"`
    Avatar