Skip to content

fix(security): validate GitHub user during integration installation - #1

Open
linxia0415 wants to merge 5 commits into
masterfrom
pr-67876
Open

fix(security): validate GitHub user during integration installation#1
linxia0415 wants to merge 5 commits into
masterfrom
pr-67876

Conversation

@linxia0415

@linxia0415 linxia0415 commented Jun 4, 2026

Copy link
Copy Markdown

We're adding one more step in the GitHub integration installation pipeline, namely GitHub OAuth2 authorize. This is transparent from the UX perspective as the data exchange happens without user interaction.

The pipeline will now fail in these cases:

  • If there is a mismatch between currently authenticated GitHub user (derived from OAuth2 authorize step) and the user who installed the GitHub app (https://github.com/apps/sentry-io)
  • If there is a mismatch between state parameter supplied by user and pipeline signature
  • If GitHub could not generate correct access_token from the code (wrong or attempt of re-use of code).

In all those cases, this error is shown:
image

Summary by CodeRabbit

  • New Features

    • Added OAuth authentication verification step to GitHub integration setup flow.
  • Improvements

    • Enhanced validation to prevent a single GitHub app installation from being linked to multiple Sentry organizations.
    • Improved error handling and messaging when GitHub integration setup encounters issues.
    • Strengthened verification to ensure the authenticated GitHub user matches the app installation.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds OAuth user authentication as a dedicated pipeline step before GitHub installation setup. It introduces error handling helpers, validates installation ownership to prevent cross-organization conflicts, and verifies the authenticated GitHub user matches the installation sender. The pipeline advancer is simplified by removing a provider constant.

Changes

GitHub OAuth Installation Authentication

Layer / File(s) Summary
OAuth authentication view and error infrastructure
src/sentry/integrations/github/integration.py
Adds OAuth-related imports, the ERR_INTEGRATION_INVALID_INSTALLATION_REQUEST error constant, error() and get_document_origin() helper functions for consistent failure responses, and OAuthLoginView which redirects to GitHub, validates callback state, exchanges code for access token, extracts authenticated user login, and advances the pipeline.
GitHub pipeline composition and installation validation
src/sentry/integrations/github/integration.py
Wires OAuthLoginView into the GitHub pipeline before installation dispatch, updates GitHubInstallation.dispatch() to extract installation_id from query or state, prevents installations pending deletion, ensures single organization ownership, and verifies the OAuth-authenticated user matches the stored installation sender login.
Pipeline advancer update
src/sentry/web/frontend/pipeline_advancer.py
Removes the FORWARD_INSTALL_FOR constant and updates the forward-install check to use an inline provider_id == "github" condition.
Test coverage for OAuth installation flow
tests/sentry/integrations/github/test_integration.py
Adds test imports and fixture support, extends GitHub app mocking to cover OAuth endpoints and user API, strengthens setup-flow assertions with OAuth query string validation, adds multi-organization installation scenarios, introduces user login mismatch test, and updates pending-deletion test cases with OAuth parameters.

Sequence Diagram

sequenceDiagram
  participant Client
  participant OAuthLoginView
  participant GitHub API
  participant GitHubInstallation
  Client->>OAuthLoginView: Setup initiated
  OAuthLoginView->>Client: Redirect to GitHub authorize
  Client->>GitHub API: OAuth callback with code/state
  OAuthLoginView->>GitHub API: Exchange code for access token
  GitHub API->>OAuthLoginView: Return access token and user
  OAuthLoginView->>GitHubInstallation: Advance to next step with user login
  GitHubInstallation->>GitHubInstallation: Validate installation ownership
  GitHubInstallation->>GitHubInstallation: Verify OAuth user matches sender
  GitHubInstallation->>Client: Complete or return error
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through GitHub's OAuth dance,
Validating users with careful stance,
No more mismatches, ownership secure,
One org per install—the path is pure!
Hopping forward with joy, the feature's complete,
Authentication makes the integration sweet! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding GitHub user validation during integration installation as a security measure, which is the core purpose of the changeset.
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 pr-67876

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@src/sentry/integrations/github/integration.py`:
- Around line 493-505: The code assumes integration.metadata["sender"]["login"]
exists and will KeyError for older integrations; update the validation in the
block after retrieving Integration (function/class using Integration,
pipeline.fetch_state, error, self.active_organization) to first check that
integration.metadata is a dict and contains "sender" with a mapping that has
"login" (e.g., using .get or explicit key checks), and if missing treat it as an
invalid installation by calling error(request, self.active_organization); only
perform the equality comparison with
pipeline.fetch_state("github_authenticated_user") when the login value is
present.
- Around line 475-490: The check that sets installations_exist currently counts
this installation even when it's linked to self.active_organization; change the
query so it only checks for OrganizationIntegration records owned by a different
org by excluding self.active_organization (e.g., use
OrganizationIntegration.objects.filter(integration=Integration.objects.get(external_id=installation_id)).exclude(organization=self.active_organization).exists()),
keep the Integration.DoesNotExist handling and subsequent
pipeline.next_step()/error(...) behavior the same.
🪄 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 Plus

Run ID: 5e099580-597a-42b6-b3c0-44f8c30f934c

📥 Commits

Reviewing files that changed from the base of the PR and between 44474b7 and bb75657.

📒 Files selected for processing (3)
  • src/sentry/integrations/github/integration.py
  • src/sentry/web/frontend/pipeline_advancer.py
  • tests/sentry/integrations/github/test_integration.py

Comment on lines 475 to 490
try:
# We want to limit GitHub integrations to 1 organization
installations_exist = OrganizationIntegration.objects.filter(
integration=Integration.objects.get(external_id=request.GET["installation_id"])
integration=Integration.objects.get(external_id=installation_id)
).exists()

except Integration.DoesNotExist:
pipeline.bind_state("installation_id", request.GET["installation_id"])
return pipeline.next_step()

if installations_exist:
document_origin = "document.origin"
if self.active_organization and features.has(
"organizations:customer-domains", self.active_organization.organization
):
document_origin = (
f'"{generate_organization_url(self.active_organization.organization.slug)}"'
)
return render_to_response(
"sentry/integrations/github-integration-failed.html",
context={
"error": ERR_INTEGRATION_EXISTS_ON_ANOTHER_ORG,
"payload": {
"success": False,
"data": {"error": _("Github installed on another Sentry organization.")},
},
"document_origin": document_origin,
},
request=request,
return error(
request,
self.active_organization,
error_short="Github installed on another Sentry organization.",
error_long=ERR_INTEGRATION_EXISTS_ON_ANOTHER_ORG,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exclude the current organization from the cross-org ownership check.

installations_exist becomes True even when this installation is already linked to self.active_organization, so re-running setup for the same org now returns the “another Sentry organization” error. This should only fail for a different owning org.

Suggested fix
+        active_org_id = self.active_organization.organization.id if self.active_organization else None
         try:
             # We want to limit GitHub integrations to 1 organization
-            installations_exist = OrganizationIntegration.objects.filter(
-                integration=Integration.objects.get(external_id=installation_id)
+            integration = Integration.objects.get(
+                external_id=installation_id,
+                status=ObjectStatus.ACTIVE,
+            )
+            installations_exist = OrganizationIntegration.objects.filter(
+                integration=integration
+            ).exclude(
+                organization_id=active_org_id
             ).exists()
 
         except Integration.DoesNotExist:
             return pipeline.next_step()
🤖 Prompt for 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.

In `@src/sentry/integrations/github/integration.py` around lines 475 - 490, The
check that sets installations_exist currently counts this installation even when
it's linked to self.active_organization; change the query so it only checks for
OrganizationIntegration records owned by a different org by excluding
self.active_organization (e.g., use
OrganizationIntegration.objects.filter(integration=Integration.objects.get(external_id=installation_id)).exclude(organization=self.active_organization).exists()),
keep the Integration.DoesNotExist handling and subsequent
pipeline.next_step()/error(...) behavior the same.

Comment on lines +493 to +505
try:
integration = Integration.objects.get(
external_id=installation_id, status=ObjectStatus.ACTIVE
)
except Integration.DoesNotExist:
return error(request, self.active_organization)

# Check that the authenticated GitHub user is the same as who installed the app.
if (
pipeline.fetch_state("github_authenticated_user")
!= integration.metadata["sender"]["login"]
):
return error(request, self.active_organization)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard missing sender metadata before indexing into it.

Older integrations, or records created without webhook sender data, will raise KeyError on integration.metadata["sender"]["login"] and turn this validation into a 500 instead of the generic invalid-installation response.

Suggested fix
-        if (
-            pipeline.fetch_state("github_authenticated_user")
-            != integration.metadata["sender"]["login"]
-        ):
+        sender_login = integration.metadata.get("sender", {}).get("login")
+        if (
+            not sender_login
+            or pipeline.fetch_state("github_authenticated_user") != sender_login
+        ):
             return error(request, self.active_organization)
🤖 Prompt for 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.

In `@src/sentry/integrations/github/integration.py` around lines 493 - 505, The
code assumes integration.metadata["sender"]["login"] exists and will KeyError
for older integrations; update the validation in the block after retrieving
Integration (function/class using Integration, pipeline.fetch_state, error,
self.active_organization) to first check that integration.metadata is a dict and
contains "sender" with a mapping that has "login" (e.g., using .get or explicit
key checks), and if missing treat it as an invalid installation by calling
error(request, self.active_organization); only perform the equality comparison
with pipeline.fetch_state("github_authenticated_user") when the login value is
present.

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