Skip to content

Fix GraphQL injection and webhook verification bypass#283

Open
SshauryaaRocks19 wants to merge 4 commits intoOWASP-BLT:mainfrom
SshauryaaRocks19:securityFix282
Open

Fix GraphQL injection and webhook verification bypass#283
SshauryaaRocks19 wants to merge 4 commits intoOWASP-BLT:mainfrom
SshauryaaRocks19:securityFix282

Conversation

@SshauryaaRocks19
Copy link
Copy Markdown

@SshauryaaRocks19 SshauryaaRocks19 commented Mar 14, 2026

Fixes #282
This pull request resolves a chained vulnerability that allowed unauthenticated attackers to execute arbitrary GraphQL queries against the GitHub API via forged webhooks.

Changes Made:

Parameterised GraphQL Queries (src/github_api.py): The fetch_multiple_prs_batch() function was redesigned to replace f-string interpolation with appropriate GraphQL variables. By securely passing variables alongside the query instead of injecting them directly into the query string, this totally mitigates the injection vector.

Webhook "Opened" Handler Validation (src/handlers.py): To appropriately handle not_found and not_modified sentinel values, the handle_github_webhook() function was updated. In order to stop attacker-controlled fake repository data from contaminating the database, the handler now explicitly verifies that the PR data is valid before calling upsert_pr().

Enforced Webhook Security (src/github_api.py): verify_github_signature() was modified to return False when GITHUB_WEBHOOK_SECRET is not configured.

All three broken links in the attack chain are now patched, protecting private repository data and preventing API rate-limit exhaustion.

Summary by CodeRabbit

  • Bug Fixes

    • Skip writing PR data when fetched data is missing or unchanged; webhook paths now return errors when PR fetch fails.
    • Fail webhook signature verification when no secret is configured.
  • Refactor

    • Consolidated multi-PR retrieval into a single batched request for improved efficiency.
    • Expanded batched PR metadata returned (status, draft, merge info, reviewers, timestamps) with fallbacks for fields not available in batch mode.

@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Mar 14, 2026

📊 Monthly Leaderboard

Hi @SshauryaaRocks19! Here's how you rank for March 2026:

Rank User Open PRs PRs (merged) PRs (closed) Reviews Comments Total
32 Ravitheja1289-dot @Ravitheja1289-dot 4 0 12 1 19 23
33 SshauryaaRocks19 @SshauryaaRocks19 1 1 0 0 5 21
34 ramansh18 @ramansh18 1 1 0 0 4 19

Scoring this month (across OWASP-BLT org): Open PRs (+1 each), Merged PRs (+10), Closed (not merged) (−2), Reviews (+5; first two per PR in-month), Comments (+2, excludes CodeRabbit). Run /leaderboard on any issue or PR to see your rank!

@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Mar 14, 2026

👋 Hi @SshauryaaRocks19!

This pull request needs a peer review before it can be merged. Please request a review from a team member who is not:

  • The PR author
  • coderabbitai
  • copilot

Once a valid peer review is submitted, this check will pass automatically. Thank you!

⚠️ Peer review enforcement is active.

@github-actions
Copy link
Copy Markdown

🍃 PR Readiness Check

Check the readiness of this PR on Leaf:
👉 Open on Leaf

Leaf reviews pull requests for operational readiness, security risks, and production-impacting changes before they ship.

@github-actions github-actions bot added the files-changed: 2 PR changes 2 files label Mar 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 14, 2026

Walkthrough

Reworks batch PR fetching to use GraphQL variables and per-PR aliases, tightens webhook handling to skip upserts for not_found/not_modified, and makes signature verification fail when no webhook secret is configured.

Changes

Cohort / File(s) Summary
Batch GraphQL fetch
src/github_api.py
Rewrote fetch_multiple_prs_batch() to build a single batched GraphQL query using per-PR variables ($owner{i}, $repo{i}, $pr{i}) and dynamic aliases (pr0, pr1, ...). Expanded fields fetched, switched request payload to use variables, and remapped alias -> PR results. Partial-data flags (e.g., missing checks/behind_by) are set when batch mode lacks fields.
Webhook signature behavior
src/github_api.py
Changed verify_github_signature() to return False when GITHUB_WEBHOOK_SECRET is unset (previously returned True).
Webhook handling / upsert guard
src/handlers.py
Handler flows updated to only call upsert_pr when fetched PR data exists and does not include not_found or not_modified. Added explicit 500 responses when PR fetch fails in affected webhook paths.

Sequence Diagram(s)

sequenceDiagram
    participant WebhookSender as Client (GitHub webhook)
    participant API as Server / Handlers
    participant Verifier as verify_github_signature
    participant Fetcher as fetch_multiple_prs_batch
    participant GH as GitHub GraphQL API
    participant DB as Database

    WebhookSender->>API: POST /api/github/webhook (payload, sig)
    API->>Verifier: verify_github_signature(payload, sig)
    Verifier-->>API: valid / invalid
    alt valid
        API->>Fetcher: request PR data (batched variables + aliases)
        Fetcher->>GH: POST GraphQL (variables + single query with aliases)
        GH-->>Fetcher: batched PR results (alias -> PR)
        Fetcher-->>API: parsed per-PR data (may include not_found/not_modified flags)
        alt PR data valid
            API->>DB: upsert_pr(per-PR data)
        else not_found / not_modified
            API-->>WebhookSender: skip upsert / return error
        end
    else invalid
        API-->>WebhookSender: reject (invalid signature)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

quality: high

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the two main security fixes: GraphQL injection elimination via parameterization and webhook verification bypass prevention.
Linked Issues check ✅ Passed All code-level requirements from #282 are met: GraphQL variables replace f-string interpolation, sentinel values are explicitly checked, and webhook verification returns False when secret is unset.
Out of Scope Changes check ✅ Passed All changes directly address the security vulnerabilities outlined in #282; no unrelated modifications detected in the file summaries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/handlers.py (2)

1177-1179: Same sentinel check should be applied here.

For consistency with the fix at line 1080, consider adding sentinel checks before upserting.

🛡️ Suggested defensive fix
             # Handle synchronized (new commits) or edited PRs - update data
             elif action in ['synchronize', 'edited']:
                 # Fetch fresh PR data
                 webhook_token = getattr(env, 'GITHUB_TOKEN', None)
                 fetched_pr_data = await fetch_pr_data(repo_owner, repo_name, pr_number, webhook_token)
-                if fetched_pr_data:
+                if fetched_pr_data and not fetched_pr_data.get('not_found') and not fetched_pr_data.get('not_modified'):
                     await upsert_pr(db, pr_url, repo_owner, repo_name, pr_number, fetched_pr_data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1177 - 1179, The fetched_pr_data result should
be validated against the same sentinel used earlier before calling upsert_pr;
locate the sentinel constant used at the earlier fix (e.g., PR_NOT_FOUND /
PR_FETCH_SENTINEL) and change the block around fetch_pr_data and upsert_pr to
only call await upsert_pr(db, pr_url, repo_owner, repo_name, pr_number,
fetched_pr_data) when fetched_pr_data is truthy and is not equal to that
sentinel value (i.e., add the same sentinel check used at line ~1080).

1154-1156: Consider applying the same sentinel checks for consistency.

The 'opened' handler at line 1080 now correctly checks for not_found and not_modified sentinels before upserting. However, this handler and the 'synchronize'/'edited' handler (lines 1177-1179) still use the simpler if fetched_pr_data: check.

While the signature verification fix in verify_github_signature() blocks the primary attack vector, applying consistent sentinel handling across all upsert paths provides defense-in-depth.

🛡️ Suggested defensive fix
             # Handle reopened PRs - re-add to tracking if it was tracked before
             elif action == 'reopened':
                 # Fetch fresh PR data
                 webhook_token = getattr(env, 'GITHUB_TOKEN', None)
                 fetched_pr_data = await fetch_pr_data(repo_owner, repo_name, pr_number, webhook_token)
-                if fetched_pr_data:
+                if fetched_pr_data and not fetched_pr_data.get('not_found') and not fetched_pr_data.get('not_modified'):
                     await upsert_pr(db, pr_url, repo_owner, repo_name, pr_number, fetched_pr_data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1154 - 1156, The upsert paths still use a
simple truthy check ("if fetched_pr_data:") which skips the defensive sentinel
checks used elsewhere; update the handlers that call fetch_pr_data (the
'opened', 'synchronize', and 'edited' handlers that call fetch_pr_data and then
upsert_pr) to explicitly check for sentinel values instead of truthiness — e.g.
replace "if fetched_pr_data:" with a check that fetched_pr_data is not equal to
the not_found and not_modified sentinels (or use "if fetched_pr_data not in
(not_found, not_modified):") before calling upsert_pr so all upsert paths
consistently ignore sentinel responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/handlers.py`:
- Around line 1177-1179: The fetched_pr_data result should be validated against
the same sentinel used earlier before calling upsert_pr; locate the sentinel
constant used at the earlier fix (e.g., PR_NOT_FOUND / PR_FETCH_SENTINEL) and
change the block around fetch_pr_data and upsert_pr to only call await
upsert_pr(db, pr_url, repo_owner, repo_name, pr_number, fetched_pr_data) when
fetched_pr_data is truthy and is not equal to that sentinel value (i.e., add the
same sentinel check used at line ~1080).
- Around line 1154-1156: The upsert paths still use a simple truthy check ("if
fetched_pr_data:") which skips the defensive sentinel checks used elsewhere;
update the handlers that call fetch_pr_data (the 'opened', 'synchronize', and
'edited' handlers that call fetch_pr_data and then upsert_pr) to explicitly
check for sentinel values instead of truthiness — e.g. replace "if
fetched_pr_data:" with a check that fetched_pr_data is not equal to the
not_found and not_modified sentinels (or use "if fetched_pr_data not in
(not_found, not_modified):") before calling upsert_pr so all upsert paths
consistently ignore sentinel responses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: OWASP-BLT/coderabbit/.coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6047af64-ba09-4301-9e2d-0886ac344172

📥 Commits

Reviewing files that changed from the base of the PR and between a22a666 and 2f99a8f.

📒 Files selected for processing (2)
  • src/github_api.py
  • src/handlers.py

@SshauryaaRocks19 SshauryaaRocks19 changed the title Security fix282 Fix GraphQL injection and webhook verification bypass Mar 14, 2026
@Kunal241207
Copy link
Copy Markdown

Good Security improvement,
Since verify_github_signature() now fails closed when the secret is missing, I just want to confirm that all webhook entry points correctly check its return value and abort processing on failure.

@Nachiket-Roy
Copy link
Copy Markdown
Contributor

Look into nitpicks too

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/handlers.py (2)

1178-1194: ⚠️ Potential issue | 🟡 Minor

Missing error handling when fetch fails for synchronize/edited actions.

Same issue as the reopened action above - if fetched_pr_data is None or a sentinel, no response is returned from this block, causing a misleading success response at line 1319.

🛡️ Proposed fix to add error handling
             return Response.new(
                 json.dumps({
                     'success': True,
                     'event': 'pr_updated',
                     'pr_id': pr_id,
                     'pr_number': pr_number,
                     'data': fetched_pr_data,
                     'message': f'PR #{pr_number} has been updated'
                 }),
                 {'headers': {'Content-Type': 'application/json'}}
             )
+        else:
+            return Response.new(
+                json.dumps({
+                    'success': False,
+                    'event': 'pr_updated',
+                    'pr_id': pr_id,
+                    'pr_number': pr_number,
+                    'message': f'Failed to fetch data for updated PR #{pr_number}'
+                }),
+                {'status': 500, 'headers': {'Content-Type': 'application/json'}}
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1178 - 1194, The block handling fetched_pr_data
for synchronize/edited actions lacks an else path when fetched_pr_data is None
or indicates failure, so requests fall through and later code returns a
misleading success; update the conditional around fetched_pr_data (the branch
that calls upsert_pr, invalidate_readiness_cache, invalidate_timeline_cache and
returns Response.new) to add an explicit error response when fetched_pr_data is
falsy or contains 'not_found'/'not_modified' (mirror the error handling used in
the reopened action), returning a JSON Response.new with success:false, a clear
error message referencing pr_number and pr_id, and appropriate headers so failed
fetches don't produce a success result.

1155-1171: ⚠️ Potential issue | 🟡 Minor

Missing error handling when fetch fails for reopened action.

If fetched_pr_data is None or a sentinel value (not_found/not_modified), the function falls through without returning, eventually reaching line 1319 which returns a misleading "no handler configured" success response. This silently masks fetch failures.

Compare to the opened action at lines 1100-1114 which properly handles this case with error notification and HTTP 500 response.

🛡️ Proposed fix to add error handling
             return Response.new(
                 json.dumps({
                     'success': True,
                     'event': 'pr_reopened',
                     'pr_id': pr_id,
                     'pr_number': pr_number,
                     'data': fetched_pr_data,
                     'message': f'PR #{pr_number} has been reopened'
                 }),
                 {'headers': {'Content-Type': 'application/json'}}
             )
+        else:
+            return Response.new(
+                json.dumps({
+                    'success': False,
+                    'event': 'pr_reopened',
+                    'pr_id': pr_id,
+                    'pr_number': pr_number,
+                    'message': f'Failed to fetch data for reopened PR #{pr_number}'
+                }),
+                {'status': 500, 'headers': {'Content-Type': 'application/json'}}
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1155 - 1171, The reopened-action branch
currently only returns on success and falls through when fetched_pr_data is None
or has not_found/not_modified, causing a misleading "no handler configured"
success; update the reopened branch (the block using fetched_pr_data, upsert_pr,
invalidate_readiness_cache, invalidate_timeline_cache and Response.new for
'pr_reopened') to mirror the opened-action error handling: detect when
fetched_pr_data is falsy or has 'not_found'/'not_modified', call the same
error/notification routine used in the opened flow (log/notify about fetch
failure), and return an HTTP 500 JSON Response with success: False and an
appropriate error message instead of falling through.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/handlers.py`:
- Around line 1178-1194: The block handling fetched_pr_data for
synchronize/edited actions lacks an else path when fetched_pr_data is None or
indicates failure, so requests fall through and later code returns a misleading
success; update the conditional around fetched_pr_data (the branch that calls
upsert_pr, invalidate_readiness_cache, invalidate_timeline_cache and returns
Response.new) to add an explicit error response when fetched_pr_data is falsy or
contains 'not_found'/'not_modified' (mirror the error handling used in the
reopened action), returning a JSON Response.new with success:false, a clear
error message referencing pr_number and pr_id, and appropriate headers so failed
fetches don't produce a success result.
- Around line 1155-1171: The reopened-action branch currently only returns on
success and falls through when fetched_pr_data is None or has
not_found/not_modified, causing a misleading "no handler configured" success;
update the reopened branch (the block using fetched_pr_data, upsert_pr,
invalidate_readiness_cache, invalidate_timeline_cache and Response.new for
'pr_reopened') to mirror the opened-action error handling: detect when
fetched_pr_data is falsy or has 'not_found'/'not_modified', call the same
error/notification routine used in the opened flow (log/notify about fetch
failure), and return an HTTP 500 JSON Response with success: False and an
appropriate error message instead of falling through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: OWASP-BLT/coderabbit/.coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 29f151e6-d026-478b-a5a1-9b1f97a66509

📥 Commits

Reviewing files that changed from the base of the PR and between 2f99a8f and 832382d.

📒 Files selected for processing (1)
  • src/handlers.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/handlers.py (2)

1172-1176: Consider adding Slack notification for consistency with 'opened' action.

The 'opened' action handler (lines 1101-1110) calls notify_slack_error when PR data fetch fails, but this 'reopened' path does not. For consistent observability and debugging, consider adding the same notification here.

♻️ Suggested change for parity with 'opened' handler
                 else:
+                    await notify_slack_error(
+                        getattr(env, 'SLACK_ERROR_WEBHOOK', ''),
+                        error_type='WebhookError',
+                        error_message='Failed to fetch PR data from GitHub',
+                        context={
+                            'handler': 'handle_github_webhook',
+                            'action': 'reopened',
+                            'pr_number': str(pr_number),
+                            'repo': f'{repo_owner}/{repo_name}',
+                        },
+                    )
                     return Response.new(
                         json.dumps({'error': 'Failed to fetch PR data from GitHub'}),
                         {'status': 500, 'headers': {'Content-Type': 'application/json'}}
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1172 - 1176, The reopened-PR error branch
returns a 500 response but lacks the Slack notify call used in the opened-PR
path; update the else branch that returns Response.new({...}) to also invoke
notify_slack_error with the same arguments used in the opened handler (i.e.,
pass the error context/PR identifier and the error message) before returning so
that notify_slack_error is called in the reopened PR flow just like in the
opened flow.

1200-1204: Same observation: Missing Slack notification for consistency.

Similar to the 'reopened' handler, this path lacks the notify_slack_error call that exists in the 'opened' handler. Consider adding it for observability parity.

♻️ Suggested change
                 else:
+                    await notify_slack_error(
+                        getattr(env, 'SLACK_ERROR_WEBHOOK', ''),
+                        error_type='WebhookError',
+                        error_message='Failed to fetch PR data from GitHub',
+                        context={
+                            'handler': 'handle_github_webhook',
+                            'action': action,
+                            'pr_number': str(pr_number),
+                            'repo': f'{repo_owner}/{repo_name}',
+                        },
+                    )
                     return Response.new(
                         json.dumps({'error': 'Failed to fetch PR data from GitHub'}),
                         {'status': 500, 'headers': {'Content-Type': 'application/json'}}
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers.py` around lines 1200 - 1204, The else branch that returns
Response.new(json.dumps({'error': 'Failed to fetch PR data from GitHub'}), ...)
is missing a call to notify_slack_error for observability; update the handler
(the same one that mirrors the 'opened'/'reopened' handlers) to invoke
notify_slack_error with a clear message and contextual data (e.g., the error
string and relevant PR/event identifiers or payload) immediately before
returning the 500 Response so Slack is notified of the failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/handlers.py`:
- Around line 1172-1176: The reopened-PR error branch returns a 500 response but
lacks the Slack notify call used in the opened-PR path; update the else branch
that returns Response.new({...}) to also invoke notify_slack_error with the same
arguments used in the opened handler (i.e., pass the error context/PR identifier
and the error message) before returning so that notify_slack_error is called in
the reopened PR flow just like in the opened flow.
- Around line 1200-1204: The else branch that returns
Response.new(json.dumps({'error': 'Failed to fetch PR data from GitHub'}), ...)
is missing a call to notify_slack_error for observability; update the handler
(the same one that mirrors the 'opened'/'reopened' handlers) to invoke
notify_slack_error with a clear message and contextual data (e.g., the error
string and relevant PR/event identifiers or payload) immediately before
returning the 500 Response so Slack is notified of the failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: OWASP-BLT/coderabbit/.coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b601ece5-4b70-4f51-a219-3744b24f4977

📥 Commits

Reviewing files that changed from the base of the PR and between 832382d and 66faafe.

📒 Files selected for processing (1)
  • src/handlers.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Chained GraphQL Injection via Forged Webhook Allows Private Repo Data Exfiltration

3 participants