Skip to content

[WEB-7778] fix(security): reject unverified OAuth provider emails to prevent ATO (Cluster E)#9289

Open
mguptahub wants to merge 2 commits into
previewfrom
web-7778/fix-oauth-unverified-email-ato
Open

[WEB-7778] fix(security): reject unverified OAuth provider emails to prevent ATO (Cluster E)#9289
mguptahub wants to merge 2 commits into
previewfrom
web-7778/fix-oauth-unverified-email-ato

Conversation

@mguptahub

@mguptahub mguptahub commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

OAuth providers can return unverified email addresses. The current code accepted them and used them to look up existing Plane accounts — allowing an attacker who controls a self-hosted Gitea or GitLab instance to assert any email and gain access to another user's account (account takeover).

GHSA-cv9p-325g-wmv5 and GHSA-hx79-5pj5-qh42 (avatar SSRF) were already fixed in PR #9163 and are not part of this change.

Changes

File Fix
adapter/error.py Add OAUTH_PROVIDER_UNVERIFIED_EMAIL error code (5124)
provider/oauth/github.py Require primary=True and verified=True (was primary-only)
provider/oauth/google.py Check verified_email field in userinfo response
provider/oauth/gitlab.py Check confirmed_at is non-null before accepting email
provider/oauth/gitea.py Remove unverified fallbacks in __get_email(); remove fast path in set_user_data() that bypassed verification

All providers now fail closed — if no verified email is available, AuthenticationException(OAUTH_PROVIDER_UNVERIFIED_EMAIL) is raised and login is rejected.

Test plan

  • GitHub login with a verified primary email — succeeds
  • GitHub login where primary email is unverified — rejected with OAUTH_PROVIDER_UNVERIFIED_EMAIL
  • Google login with verified_email: true — succeeds
  • Google login with verified_email: false (mocked) — rejected
  • GitLab login with confirmed_at set — succeeds
  • GitLab login with confirmed_at: null (mocked self-hosted) — rejected
  • Gitea login with verified email — succeeds
  • Gitea login where all emails are unverified — rejected with OAUTH_PROVIDER_UNVERIFIED_EMAIL
  • Gitea login where only an unverified primary email exists (no verified fallback) — rejected

Fixes GHSA-7j95-vh8g-f365 (critical).

Co-authored-by: Plane AI noreply@plane.so

Summary by CodeRabbit

  • Bug Fixes
    • OAuth authentication now requires verified email addresses across GitHub, GitLab, Google, and Gitea providers.
    • Users attempting to sign in with unverified (or missing verified) email addresses will be blocked with a clear authentication error.
  • New Features
    • Added a dedicated authentication error code for “unverified OAuth provider email” to improve error reporting consistency.

…prevent ATO

An attacker controlling a self-hosted OAuth provider (Gitea, GitLab) could
assert any email address in the OAuth response and be matched to an existing
Plane account, bypassing authentication entirely.

- Add OAUTH_PROVIDER_UNVERIFIED_EMAIL (5124) error code
- GitHub: require both primary=True AND verified=True on email (was primary-only)
- Google: check verified_email=False field in userinfo response
- GitLab: check confirmed_at is non-null before accepting email
- Gitea __get_email: remove unverified fallbacks (primary-unverified, any-unverified)
- Gitea set_user_data: remove fast-path using .email from user object (no
  verification flag); always go through __get_email() which enforces verified

Fixes GHSA-7j95-vh8g-f365 (critical ATO).
Note: GHSA-cv9p-325g-wmv5 and GHSA-hx79-5pj5-qh42 (avatar SSRF) were
already fixed in PR #9163.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings June 22, 2026 10:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@makeplane

makeplane Bot commented Jun 22, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d3f4f94-0d3a-4dbd-bce8-5bd7934bff60

📥 Commits

Reviewing files that changed from the base of the PR and between 36cef21 and 3daec94.

📒 Files selected for processing (2)
  • apps/api/plane/authentication/provider/oauth/gitea.py
  • apps/api/plane/authentication/provider/oauth/google.py

📝 Walkthrough

Walkthrough

Adds OAUTH_PROVIDER_UNVERIFIED_EMAIL (code 5124) to the authentication error dictionary, then enforces verified-email checks in GitHub, GitLab, Gitea, and Google OAuth flows.

Changes

OAuth Unverified Email Rejection

Layer / File(s) Summary
New unverified-email error code
apps/api/plane/authentication/adapter/error.py
Adds "OAUTH_PROVIDER_UNVERIFIED_EMAIL": 5124 to AUTHENTICATION_ERROR_CODES.
Verified-email checks in OAuth providers
apps/api/plane/authentication/provider/oauth/gitea.py, apps/api/plane/authentication/provider/oauth/github.py, apps/api/plane/authentication/provider/oauth/gitlab.py, apps/api/plane/authentication/provider/oauth/google.py
GitHub requires a primary verified email, Gitea requests read:user and only accepts verified emails, GitLab rejects missing confirmed_at, and Google rejects responses where verified_email is not True.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop hop, no sneaky mail shall pass,
Verified emails now lead the class.
Gitea, GitHub, GitLab, and Google too,
Only the true-blue inbox gets through.
Error 5124 guards the warren's door ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main security fix to reject unverified OAuth emails.
Description check ✅ Passed The description covers the required summary, change details, and test plan; only optional template sections are missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web-7778/fix-oauth-unverified-email-ato

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/authentication/provider/oauth/gitea.py`:
- Around line 158-161: The __get_email() method calls the Gitea API endpoint GET
/api/v1/user/emails, which requires the read:user OAuth2 scope. The current
scope configuration in the Gitea OAuth provider (around line 22) only includes
standard OIDC scopes (openid email profile) and lacks the necessary read:user
scope. Update the scope variable in the Gitea OAuth configuration to include the
read:user scope by adding it to the existing scope string, changing it from
"openid email profile" to "openid email profile read:user", so that the
__get_email() method can successfully authenticate and retrieve verified email
information.

In `@apps/api/plane/authentication/provider/oauth/google.py`:
- Around line 105-108: The email verification check in the Google OAuth provider
is using a default value of True when the verified_email field is missing, which
treats absent verification claims as trusted and violates the fail-closed
security principle. Change the default parameter in the
user_info_response.get("verified_email", True) call from True to False so that
when Google omits the verified_email field, the email is properly treated as
unverified and rejected, requiring explicit verification signals to proceed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5a09811-d7c3-49cf-aae3-80f2fe6b0daf

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0746b and 36cef21.

📒 Files selected for processing (5)
  • apps/api/plane/authentication/adapter/error.py
  • apps/api/plane/authentication/provider/oauth/gitea.py
  • apps/api/plane/authentication/provider/oauth/github.py
  • apps/api/plane/authentication/provider/oauth/gitlab.py
  • apps/api/plane/authentication/provider/oauth/google.py

Comment thread apps/api/plane/authentication/provider/oauth/gitea.py
Comment thread apps/api/plane/authentication/provider/oauth/google.py Outdated
…ogle verified_email

Gitea's /api/v1/user/emails endpoint requires the read:user granular
scope — openid+email+profile alone is insufficient and __get_email()
would return a 401/403. Add read:user to the scope string.

Google: change default from True to fail-closed (is not True) so a
userinfo response that omits verified_email is rejected rather than
trusted. The service-account justification was incorrect — service
accounts do not go through the interactive OAuth2 callback flow.

Co-authored-by: Plane AI <noreply@plane.so>
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