-
-
Notifications
You must be signed in to change notification settings - Fork 138
Improve validation and robustness of GitHub webhook handler #276
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: main
Are you sure you want to change the base?
Changes from 1 commit
2e2fd1a
4e08d23
3422790
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,17 +7,24 @@ | |||||||||||||||||||||||||||
| from app.core.events.base import BaseEvent | ||||||||||||||||||||||||||||
| from app.core.handler.handler_registry import HandlerRegistry | ||||||||||||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||||||||||||
| from typing import Optional, Dict, Any | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| router = APIRouter() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class RepoRequest(BaseModel): | ||||||||||||||||||||||||||||
| repo_url: str | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class GitHubWebhookPayload(BaseModel): | ||||||||||||||||||||||||||||
| action: Optional[str] = None | ||||||||||||||||||||||||||||
| sender: Optional[Dict[str, Any]] = None | ||||||||||||||||||||||||||||
| pull_request: Optional[Dict[str, Any]] = None | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| logging.basicConfig(level=logging.INFO) | ||||||||||||||||||||||||||||
| handler_registry = HandlerRegistry() | ||||||||||||||||||||||||||||
| event_bus = EventBus(handler_registry) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Sample handler function to process events | ||||||||||||||||||||||||||||
| async def sample_handler(event: BaseEvent): | ||||||||||||||||||||||||||||
| logging.info(f"Handler received event: {event.event_type} with data: {event.raw_data}") | ||||||||||||||||||||||||||||
|
|
@@ -35,17 +42,25 @@ def register_event_handlers(): | |||||||||||||||||||||||||||
| event_bus.register_handler(EventType.PR_COMMENTED, sample_handler, PlatformType.GITHUB) | ||||||||||||||||||||||||||||
| event_bus.register_handler(EventType.PR_MERGED, sample_handler, PlatformType.GITHUB) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| register_event_handlers() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @router.post("/github/webhook") | ||||||||||||||||||||||||||||
| async def github_webhook(request: Request): | ||||||||||||||||||||||||||||
| payload = await request.json() | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| raw_payload = await request.json() | ||||||||||||||||||||||||||||
| payload = GitHubWebhookPayload(**raw_payload) | ||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||
| logging.error(f"Invalid webhook payload: {e}") | ||||||||||||||||||||||||||||
| raise HTTPException(status_code=400, detail="Invalid webhook payload") | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Improve exception handling per static analysis findings. Three valid concerns flagged by Ruff:
🔧 Proposed fix+ from pydantic import ValidationError
try:
raw_payload = await request.json()
payload = GitHubWebhookPayload(**raw_payload)
- except Exception as e:
- logging.error(f"Invalid webhook payload: {e}")
- raise HTTPException(status_code=400, detail="Invalid webhook payload")
+ except (ValueError, ValidationError) as e:
+ logging.exception("Invalid webhook payload")
+ raise HTTPException(status_code=400, detail="Invalid webhook payload") from e
📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.0)[warning] 52-52: Do not catch blind exception: (BLE001) [warning] 53-53: Use Replace with (TRY400) [warning] 54-54: Within an (B904) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event_header = request.headers.get("X-GitHub-Event") | ||||||||||||||||||||||||||||
| logging.info(f"Received GitHub event: {event_header}") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| event_type = None | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Handle issue events | ||||||||||||||||||||||||||||
| if event_header == "issues": | ||||||||||||||||||||||||||||
| action = payload.get("action") | ||||||||||||||||||||||||||||
| action = payload.action | ||||||||||||||||||||||||||||
| if action == "opened": | ||||||||||||||||||||||||||||
| event_type = EventType.ISSUE_CREATED | ||||||||||||||||||||||||||||
| elif action == "closed": | ||||||||||||||||||||||||||||
|
|
@@ -55,41 +70,41 @@ async def github_webhook(request: Request): | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Handle issue comment events | ||||||||||||||||||||||||||||
| elif event_header == "issue_comment": | ||||||||||||||||||||||||||||
| action = payload.get("action") | ||||||||||||||||||||||||||||
| action = payload.action | ||||||||||||||||||||||||||||
| if action == "created": | ||||||||||||||||||||||||||||
| event_type = EventType.ISSUE_COMMENTED | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Handle pull request events | ||||||||||||||||||||||||||||
| elif event_header == "pull_request": | ||||||||||||||||||||||||||||
| action = payload.get("action") | ||||||||||||||||||||||||||||
| action = payload.action | ||||||||||||||||||||||||||||
| if action == "opened": | ||||||||||||||||||||||||||||
| event_type = EventType.PR_CREATED | ||||||||||||||||||||||||||||
| elif action == "edited": | ||||||||||||||||||||||||||||
| event_type = EventType.PR_UPDATED | ||||||||||||||||||||||||||||
| elif action == "closed": | ||||||||||||||||||||||||||||
| # Determine if the PR was merged or simply closed | ||||||||||||||||||||||||||||
| if payload.get("pull_request", {}).get("merged"): | ||||||||||||||||||||||||||||
| if payload.pull_request and payload.pull_request.get("merged"): | ||||||||||||||||||||||||||||
| event_type = EventType.PR_MERGED | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| logging.info("Pull request closed without merge; no event dispatched.") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Handle pull request comment events | ||||||||||||||||||||||||||||
| elif event_header in ["pull_request_review_comment", "pull_request_comment"]: | ||||||||||||||||||||||||||||
| action = payload.get("action") | ||||||||||||||||||||||||||||
| action = payload.action | ||||||||||||||||||||||||||||
| if action == "created": | ||||||||||||||||||||||||||||
| event_type = EventType.PR_COMMENTED | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: GitHub’s official documentation for Key points from the docs:
Citations:
🏁 Script executed: # Find and check the EventType enum in the codebase
find . -type f -name "*.py" | xargs rg -l "EventType" | head -5Repository: AOSSIE-Org/Devr.AI Length of output: 264 🏁 Script executed: # Find EventType enum definition
rg -n "class EventType|enum EventType" backend/Repository: AOSSIE-Org/Devr.AI Length of output: 125 🌐 Web query:
💡 Result: For GitHub webhooks (and the same “activity types” you can filter on in GitHub Actions), the
Notes: “Approved” / “changes_requested” are not Sources 🏁 Script executed: # Read the EventType enum to check available event types
cat -n backend/app/core/events/enums.pyRepository: AOSSIE-Org/Devr.AI Length of output: 1217
GitHub sends 🔧 Suggested fix- elif event_header in ["pull_request_review_comment", "pull_request_comment"]:
+ elif event_header in ["pull_request_review_comment"]:
action = payload.action
if action == "created":
event_type = EventType.PR_COMMENTEDAnd optionally add a separate handler for review submissions: elif event_header == "pull_request_review":
action = payload.action
if action == "submitted":
event_type = EventType.PR_REVIEWED📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Dispatch the event if we have a matching type | ||||||||||||||||||||||||||||
| if event_type: | ||||||||||||||||||||||||||||
| event = BaseEvent( | ||||||||||||||||||||||||||||
| id=str(uuid.uuid4()), | ||||||||||||||||||||||||||||
| actor_id=str(payload.get("sender", {}).get("id", "unknown")), | ||||||||||||||||||||||||||||
| actor_id=str(payload.sender.get("id", "unknown")) if payload.sender else "unknown", | ||||||||||||||||||||||||||||
| event_type=event_type, | ||||||||||||||||||||||||||||
| platform=PlatformType.GITHUB, | ||||||||||||||||||||||||||||
| raw_data=payload | ||||||||||||||||||||||||||||
| raw_data=raw_payload | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| await event_bus.dispatch(event) | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| logging.info(f"No matching event type for header: {event_header} with action: {payload.get('action')}") | ||||||||||||||||||||||||||||
| logging.warning(f"No matching event type for header: {event_header} with action: {payload.action}") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return {"status": "ok"} | ||||||||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 44
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 44
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 4624
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 83
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 645
🏁 Script executed:
Repository: AOSSIE-Org/Devr.AI
Length of output: 44
Missing webhook signature verification (HMAC).
The endpoint does not verify the
X-Hub-Signature-256header against a shared secret. Without this, anyone can POST to this endpoint and trigger event processing. This is a significant security gap for a production webhook handler. Consider verifying the signature before processing the payload.🔧 Example approach
Note: when reading the body for HMAC verification, you'll need to parse JSON from the raw bytes yourself rather than calling
request.json()twice.🤖 Prompt for AI Agents