-
Notifications
You must be signed in to change notification settings - Fork 0
GitHub OAuth Security Enhancement #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: oauth-state-vulnerable
Are you sure you want to change the base?
Conversation
…#67876) 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: 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the security of GitHub OAuth integration by implementing user authentication verification to prevent unauthorized installation hijacking.
- Adds OAuth login step to verify the GitHub user's identity before proceeding with integration setup
- Implements user mismatch validation to ensure the authenticated user matches the app installer
- Refactors error handling with a unified error response function
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tests/sentry/integrations/github/test_integration.py | Adds comprehensive test coverage for OAuth flow, user mismatch scenarios, and updates existing tests to handle the new authentication step |
| src/sentry/web/frontend/pipeline_advancer.py | Removes the global FORWARD_INSTALL_FOR constant and inlines the GitHub-specific logic for better maintainability |
| src/sentry/integrations/github/integration.py | Implements new OAuthLoginView for user authentication, adds user validation in GitHubInstallation, and introduces unified error handling |
| ) | ||
|
|
||
| auth_header = responses.calls[0].request.headers["Authorization"] | ||
| auth_header = responses.calls[2].request.headers["Authorization"] |
Copilot
AI
Jul 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded index responses.calls[2] creates a brittle test that depends on the exact number and order of HTTP calls. Consider using a more robust approach like filtering calls by URL or method, or using a helper method to find the specific call you need to verify.
| auth_header = responses.calls[2].request.headers["Authorization"] | |
| auth_call = next( | |
| call for call in responses.calls if "Authorization" in call.request.headers | |
| ) | |
| auth_header = auth_call.request.headers["Authorization"] |
| ) | ||
|
|
||
| # At this point, we are past the GitHub "authorize" step | ||
| if request.GET.get("state") != pipeline.signature: |
Copilot
AI
Jul 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state parameter comparison should use a constant-time comparison to prevent timing attacks. Consider using secrets.compare_digest() or Django's constant_time_compare() function instead of the != operator.
| if request.GET.get("state") != pipeline.signature: | |
| from django.utils.crypto import constant_time_compare | |
| if not constant_time_compare(request.GET.get("state"), pipeline.signature): |
| except Exception: | ||
| payload = {} |
Copilot
AI
Jul 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching all exceptions with a bare Exception clause can mask important errors and make debugging difficult. Consider catching specific exceptions like UnicodeDecodeError, ValueError, or other expected exceptions that could occur during response parsing.
| except Exception: | |
| payload = {} | |
| except UnicodeDecodeError as e: | |
| logger.error("Failed to decode response body: %s", e) | |
| payload = {} | |
| except ValueError as e: | |
| logger.error("Failed to parse response body into a dictionary: %s", e) | |
| payload = {} |
| if ( | ||
| pipeline.fetch_state("github_authenticated_user") | ||
| != integration.metadata["sender"]["login"] |
Copilot
AI
Jul 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user login comparison should use a constant-time comparison to prevent timing attacks that could leak information about valid usernames. Consider using secrets.compare_digest() or Django's constant_time_compare() function.
| if ( | |
| pipeline.fetch_state("github_authenticated_user") | |
| != integration.metadata["sender"]["login"] | |
| from secrets import compare_digest | |
| if not compare_digest( | |
| str(pipeline.fetch_state("github_authenticated_user")), | |
| str(integration.metadata["sender"]["login"]), |
Test 4