Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/plane/authentication/adapter/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
"GITEA_OAUTH_PROVIDER_ERROR": 5123,
"OAUTH_PROVIDER_UNVERIFIED_EMAIL": 5124,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,
Expand Down
22 changes: 12 additions & 10 deletions apps/api/plane/authentication/provider/oauth/gitea.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

class GiteaOAuthProvider(OauthAdapter):
provider = "gitea"
scope = "openid email profile"
scope = "openid email profile read:user"

def __init__(self, request, code=None, state=None, callback=None):
(GITEA_CLIENT_ID, GITEA_CLIENT_SECRET, GITEA_HOST) = get_configuration_value(
Expand Down Expand Up @@ -130,15 +130,17 @@ def __get_email(self, headers):
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
error_message="GITEA_OAUTH_PROVIDER_ERROR: No emails found",
)
# Prefer primary+verified, then any verified, then primary, else first
# Prefer primary+verified, then any verified. Never fall back to an unverified
# email — an attacker with a self-hosted Gitea instance could assert any address
# to take over an existing account (GHSA-7j95-vh8g-f365).
email = next((e.get("email") for e in emails_response if e.get("primary") and e.get("verified")), None)
if not email:
email = next((e.get("email") for e in emails_response if e.get("verified")), None)
if not email:
email = next((e.get("email") for e in emails_response if e.get("primary")), None)
if not email and emails_response:
# If no primary email, use the first one
email = emails_response[0].get("email")
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["OAUTH_PROVIDER_UNVERIFIED_EMAIL"],
error_message="OAUTH_PROVIDER_UNVERIFIED_EMAIL",
)
return email
except requests.RequestException:
raise AuthenticationException(
Expand All @@ -153,10 +155,10 @@ def set_user_data(self):
"Accept": "application/json",
}

# Get email if not provided in user info
email = user_info_response.get("email")
if not email:
email = self.__get_email(headers=headers)
# Always use __get_email() which enforces the verified-email requirement.
# The user object's .email field carries no verification flag, so it cannot
# be trusted directly (GHSA-7j95-vh8g-f365).
email = self.__get_email(headers=headers)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

super().set_user_data(
{
Expand Down
13 changes: 9 additions & 4 deletions apps/api/plane/authentication/provider/oauth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,17 @@ def __get_email(self, headers):
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
)
email = next((email["email"] for email in emails_response if email["primary"]), None)
# Require both primary AND verified — an unverified primary email can be
# exploited to take over an existing account (GHSA-7j95-vh8g-f365).
email = next(
(e["email"] for e in emails_response if e.get("primary") and e.get("verified")),
None,
)
if not email:
self.logger.error("No primary email found for user")
self.logger.error("No primary verified email found for GitHub user")
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
error_code=AUTHENTICATION_ERROR_CODES["OAUTH_PROVIDER_UNVERIFIED_EMAIL"],
error_message="OAUTH_PROVIDER_UNVERIFIED_EMAIL",
)
return email
except requests.RequestException:
Expand Down
7 changes: 7 additions & 0 deletions apps/api/plane/authentication/provider/oauth/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ def set_token_data(self):

def set_user_data(self):
user_info_response = self.get_user_response()
# confirmed_at is null/absent for unverified GitLab accounts. Reject them to
# prevent ATO via self-hosted GitLab with unverified emails (GHSA-7j95-vh8g-f365).
if not user_info_response.get("confirmed_at"):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["OAUTH_PROVIDER_UNVERIFIED_EMAIL"],
error_message="OAUTH_PROVIDER_UNVERIFIED_EMAIL",
)
email = user_info_response.get("email")
super().set_user_data(
{
Expand Down
8 changes: 8 additions & 0 deletions apps/api/plane/authentication/provider/oauth/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ def set_token_data(self):

def set_user_data(self):
user_info_response = self.get_user_response()
# Reject unverified emails — an attacker-controlled provider could otherwise assert
# any email to match an existing account (GHSA-7j95-vh8g-f365). Fail closed: treat
# an absent verified_email claim the same as verified_email=false.
if user_info_response.get("verified_email") is not True:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["OAUTH_PROVIDER_UNVERIFIED_EMAIL"],
error_message="OAUTH_PROVIDER_UNVERIFIED_EMAIL",
)
user_data = {
"email": user_info_response.get("email"),
"user": {
Expand Down
Loading