You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Security: Implement GitHub Webhook Signature Verification and Idempotency Guard
Context & Importance
As part of the backend development and integration of GitHub webhooks for AOSSIE's InPactAI (following up on the requirements discussed in #293), it is critical to protect the platform's API endpoints from malicious or unintended actions. Since InPactAI manages open-source contribution flows, active task tracking, and platform-wide assignment states, our webhook handlers represent a critical attack surface.
Without robust validation layers, the application is highly vulnerable to:
Spam Event Injections: Attackers can forge webhooks, spoofing GitHub's payload format to arbitrarily assign issues/PRs or alter contributor statuses without authentication.
Replay Attacks (Duplicate Events): Network delays, timeouts, or intentional malicious delivery replays can trigger duplicate handlers, leading to database state corruption and invalid active contributor stats.
Over-Assignment / Spam Claiming: Bad actors or spam bots could exploit the platform by claiming multiple concurrent assignments to block genuine contributors.
Implementing these webhook security guards ensures complete integrity of contributor tracking and keeps the platform secure, robust, and idempotent.
1. System Architecture & Event Flow
The sequence below illustrates the lifecycle of a secure, authenticated, and deduplicated webhook request as it travels from the GitHub API through our FastAPI application down to the database:
sequenceDiagram
autonumber
participant GitHub as GitHub API
participant Middleware as security_guard (FastAPI)
participant DB as Database (Postgres)
participant Handler as Webhook Service
GitHub->>Middleware: POST /routes/github/webhook (with X-Hub-Signature-256)
rect rgb(240, 248, 255)
note right of Middleware: Step 1: Authentication Guard
Middleware->>Middleware: Verify HMAC SHA-256 Signature
alt Invalid Signature
Middleware-->>GitHub: 403 Forbidden (Aborted)
end
end
rect rgb(245, 245, 245)
note right of Middleware: Step 2: Replay/Deduplication Guard
Middleware->>DB: Query X-GitHub-Delivery ID
alt Already Processed
DB-->>Middleware: Duplicate Detected
Middleware-->>GitHub: 200 OK (Ignored/No-op)
end
Middleware->>DB: Save X-GitHub-Delivery ID as 'pending'
end
rect rgb(255, 245, 238)
note right of Handler: Step 3: Assignment & Spam Guard
Middleware->>Handler: Forward Validated Payload
Handler->>Handler: Validate Repo Whitelist & Collaborator Permissions
Handler->>Handler: Check Assignee Concurrent Assignment Limits
alt Spam / Over-assignment Detected
Handler-->>GitHub: 400 Bad Request / 200 OK (Log Spam Warning)
end
end
Handler->>DB: Update Issue/PR Status & Record Assignment
Handler-->>GitHub: 200 OK (Success)
Loading
2. Webhook Authentication Guard (HMAC SHA-256)
GitHub signs each webhook payload with a cryptographic hash using a pre-configured secret. We must calculate the signature of the raw incoming request body and match it with the X-Hub-Signature-256 header.
Security Best Practices
Raw Body Requirement: Always use the raw byte body to compute the HMAC signature; parsing and re-serializing JSON may alter whitespace, breaking validation.
Timing-Safe Comparison: Use hmac.compare_digest to compare signatures, preventing side-channel timing attacks.
Secret Configuration: Store the webhook secret securely using environment variables (GITHUB_WEBHOOK_SECRET).
Python / FastAPI Implementation
importhmacimporthashlibimportosfromfastapiimportRequest, HTTPException, Security, statusGITHUB_WEBHOOK_SECRET=os.getenv("GITHUB_WEBHOOK_SECRET")
asyncdefverify_github_signature(request: Request):
""" FastAPI Dependency to verify the incoming X-Hub-Signature-256 header. Raises 403 Forbidden for unauthorized requests. """ifnotGITHUB_WEBHOOK_SECRET:
raiseHTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Server misconfiguration: GitHub Webhook Secret is missing."
)
signature_header=request.headers.get("X-Hub-Signature-256")
ifnotsignature_header:
raiseHTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Missing X-Hub-Signature-256 header."
)
# Header is in the format "sha256=HEX_SIGNATURE"parts=signature_header.split("=")
iflen(parts) !=2orparts[0] !="sha256":
raiseHTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid signature header format. Only sha256 is supported."
)
signature_to_verify=parts[1]
# Get the raw request payloadraw_body=awaitrequest.body()
# Calculate expected signaturecomputed_signature=hmac.new(
key=GITHUB_WEBHOOK_SECRET.encode("utf-8"),
msg=raw_body,
digestmod=hashlib.sha256
).hexdigest()
# Timing-safe string comparisonifnothmac.compare_digest(computed_signature, signature_to_verify):
raiseHTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Signature validation failed. Unauthenticated source."
)
3. Idempotency Guard (Deduplication)
GitHub may occasionally retry a webhook request (e.g., due to network timeouts, connection drops, or cold startup delays in the backend), leading to duplicate issue/PR processings.
To guarantee exactly-once processing, we implement an Idempotency Guard:
Every payload includes a unique delivery GUID in the X-GitHub-Delivery header.
We query the database to see if X-GitHub-Delivery exists in our processed_webhooks table.
If it exists, we immediately terminate processing with 200 OK (to prevent GitHub from marking it as failed).
If it is new, we store the UUID with a status of processing and proceed.
4. Assignment Validation & Spam Prevention Logic
Once authenticated and deduplicated, we validate the event metadata to reject bad actors, spam bots, and unauthorized changes.
A. Repository Whitelisting
To prevent cross-repository injection attacks, we only accept events from repositories that are explicitly registered on the InPactAI platform:
Match payload["repository"]["id"] or payload["repository"]["full_name"] against our registered repository configurations.
B. Assignment Authority Guard
In a collaborative open-source platform, users shouldn't be allowed to maliciously trigger assignment webhooks using fake setups or mock events.
Check the Actor: The payload["sender"]["login"] represents the actor who performed the assignment.
Authorization Check: Query the GitHub API (or check a cached registry of repository collaborators) to verify if the actor has admin, write, or authorized maintainer privileges on that repository.
C. Contributor Concurrency Limit (Spam Claiming)
A common spam pattern in GSoC/hackathons is a single user claiming 5+ issues at once to block others, then abandoning them.
Limit Active Assignments: Restrict a single contributor to a maximum of 2 active assigned issues/PRs across the platform.
Enforcement: If a webhook reports a new assignment for a contributor who already has active assignments over the threshold, either:
Comment automatically on the issue using a bot to unassign them.
Reject the sync internally and flag a warning.
D. Event Context Validation
Ensure that the issue state matches the event action:
For issues.assigned events, verify that the issue is currently open.
For pull_request events, reject assignment attempts if the PR is already closed or merged.
5. SQLAlchemy Database Schemas
To support these security guards, we define three tables:
processed_webhooks: Stores delivery IDs for idempotency checks.
registered_repositories: Stores platform-approved repositories and their optional per-repo credentials.
active_assignments: Tracks active work to enforce concurrency and claim integrity.
fromsqlalchemyimportColumn, String, Integer, BigInteger, DateTime, ForeignKey, Boolean, UniqueConstraintfromsqlalchemy.ormimportrelationshipfromdatetimeimportdatetimefromapp.db.dbimportBaseclassProcessedWebhook(Base):
""" Idempotency Table to track processed webhook deliveries and prevent replay attacks. Deliveries should be pruned periodically (e.g., via cron) after 7 days. """__tablename__="processed_webhooks"delivery_id=Column(String(50), primary_key=True, index=True) # UUID format from X-GitHub-Deliveryevent_type=Column(String(50), nullable=False) # from X-GitHub-Eventstatus=Column(String(20), default="processing") # processing, completed, failedprocessed_at=Column(DateTime, default=datetime.utcnow, index=True)
classRegisteredRepository(Base):
""" White-list of repositories integrated with the InPactAI platform. """__tablename__="registered_repositories"id=Column(BigInteger, primary_key=True) # GitHub's repository IDfull_name=Column(String(255), unique=True, nullable=False) # owner/repo-nameis_active=Column(Boolean, default=True)
created_at=Column(DateTime, default=datetime.utcnow)
assignments=relationship("ActiveAssignment", back_populates="repository")
classActiveAssignment(Base):
""" Table to track active contributor assignments. Prevents spam over-claiming and provides state tracking. """__tablename__="active_assignments"id=Column(String, primary_key=True) # Platform custom assignment UUIDrepository_id=Column(BigInteger, ForeignKey("registered_repositories.id"), nullable=False)
issue_number=Column(Integer, nullable=False) # Issue/PR numbertarget_type=Column(String(10), nullable=False) # 'issue' or 'pull_request'assignee_github_id=Column(BigInteger, nullable=False, index=True)
assignee_username=Column(String(100), nullable=False)
assigned_by_github_id=Column(BigInteger, nullable=False) # The moderator/bot who performed assignmentassigned_at=Column(DateTime, default=datetime.utcnow)
status=Column(String(20), default="active", index=True) # active, completed, abandonedrepository=relationship("RegisteredRepository", back_populates="assignments")
__table_args__= (
UniqueConstraint('repository_id', 'issue_number', 'assignee_github_id', name='_repo_issue_assignee_uc'),
)
6. End-to-End FastAPI Router Blueprint
Integrating all security layers together, here is the robust router blueprint for /routes/github:
fromfastapiimportAPIRouter, Request, Depends, status, Responsefromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemy.futureimportselectfromapp.db.dbimportget_dbfrom .securityimportverify_github_signaturefrom ..modelsimportProcessedWebhook, RegisteredRepository, ActiveAssignmentfromdatetimeimportdatetimerouter=APIRouter(prefix="/github", tags=["GitHub Integration"])
@router.post("/webhook",dependencies=[Depends(verify_github_signature)],status_code=status.HTTP_200_OK)asyncdefhandle_github_webhook(
request: Request,
db: AsyncSession=Depends(get_db)
):
# 1. Fetch Webhook Metadata headersdelivery_id=request.headers.get("X-GitHub-Delivery")
event_type=request.headers.get("X-GitHub-Event")
ifnotdelivery_id:
returnResponse(status_code=status.HTTP_400_BAD_REQUEST, content="Missing Delivery ID Header.")
# 2. Idempotency Check (Deduplication)stmt=select(ProcessedWebhook).where(ProcessedWebhook.delivery_id==delivery_id)
result=awaitdb.execute(stmt)
existing_delivery=result.scalars().first()
ifexisting_delivery:
# Deliveries already successfully recorded return 200 immediately to avoid hook timeout retriesreturn {"status": "ignored", "reason": f"Event {delivery_id} already processed."}
# Record the delivery to lock processingwebhook_log=ProcessedWebhook(delivery_id=delivery_id, event_type=event_type, status="processing")
db.add(webhook_log)
awaitdb.commit()
# 3. Parse JSON Bodypayload=awaitrequest.json()
try:
# 4. Whitelist Validationrepo_id=payload.get("repository", {}).get("id")
repo_stmt=select(RegisteredRepository).where(RegisteredRepository.id==repo_id)
repo_result=awaitdb.execute(repo_stmt)
repo=repo_result.scalars().first()
ifnotrepoornotrepo.is_active:
webhook_log.status="ignored"awaitdb.commit()
return {"status": "ignored", "reason": "Repository is not registered or is inactive."}
# 5. Route Specific Event Handlersifevent_type=="issues":
action=payload.get("action")
ifactionin ["assigned", "unassigned"]:
awaitprocess_issue_assignment(payload, db)
elifevent_type=="pull_request":
action=payload.get("action")
ifactionin ["assigned", "unassigned", "opened", "closed"]:
awaitprocess_pr_assignment(payload, db)
# Mark log as completedwebhook_log.status="completed"awaitdb.commit()
return {"status": "success", "delivery_id": delivery_id}
exceptExceptionase:
# Mark log as failed to allow debug visibilitywebhook_log.status="failed"awaitdb.commit()
return {"status": "error", "message": str(e)}
asyncdefprocess_issue_assignment(payload: dict, db: AsyncSession):
""" Validates assignment details, checks active claim thresholds and handles storage. """action=payload.get("action")
issue=payload.get("issue", {})
assignee=payload.get("assignee", {})
sender=payload.get("sender", {})
repo_id=payload.get("repository", {}).get("id")
issue_num=issue.get("number")
ifaction=="assigned":
# Check active assignment count for this contributor to prevent claiming spamstmt=select(ActiveAssignment).where(
ActiveAssignment.assignee_github_id==assignee.get("id"),
ActiveAssignment.status=="active"
)
res=awaitdb.execute(stmt)
active_assignments=res.scalars().all()
# Enforce Concurrency ThresholdMAX_CONCURRENT_ASSIGNMENTS=2iflen(active_assignments) >=MAX_CONCURRENT_ASSIGNMENTS:
# OPTIONAL: trigger GitHub Comment API / dispatch bot action to post warning on the issue thread# logger.warning(f"User {assignee.get('login')} exceeded concurrency threshold limit.")pass# Save assignmentnew_assignment=ActiveAssignment(
id=f"assign-{repo_id}-{issue_num}-{assignee.get('id')}",
repository_id=repo_id,
issue_number=issue_num,
target_type="issue",
assignee_github_id=assignee.get("id"),
assignee_username=assignee.get("login"),
assigned_by_github_id=sender.get("id"),
status="active"
)
db.add(new_assignment)
awaitdb.commit()
elifaction=="unassigned":
# Resolve active assignmentstmt=select(ActiveAssignment).where(
ActiveAssignment.repository_id==repo_id,
ActiveAssignment.issue_number==issue_num,
ActiveAssignment.assignee_github_id==assignee.get("id"),
ActiveAssignment.status=="active"
)
res=awaitdb.execute(stmt)
assignment=res.scalars().first()
ifassignment:
assignment.status="abandoned"awaitdb.commit()
asyncdefprocess_pr_assignment(payload: dict, db: AsyncSession):
# Similar validation flow mapping PR hooks to active assignments table.pass
Security: Implement GitHub Webhook Signature Verification and Idempotency Guard
Context & Importance
As part of the backend development and integration of GitHub webhooks for AOSSIE's InPactAI (following up on the requirements discussed in #293), it is critical to protect the platform's API endpoints from malicious or unintended actions. Since InPactAI manages open-source contribution flows, active task tracking, and platform-wide assignment states, our webhook handlers represent a critical attack surface.
Without robust validation layers, the application is highly vulnerable to:
Implementing these webhook security guards ensures complete integrity of contributor tracking and keeps the platform secure, robust, and idempotent.
1. System Architecture & Event Flow
The sequence below illustrates the lifecycle of a secure, authenticated, and deduplicated webhook request as it travels from the GitHub API through our FastAPI application down to the database:
sequenceDiagram autonumber participant GitHub as GitHub API participant Middleware as security_guard (FastAPI) participant DB as Database (Postgres) participant Handler as Webhook Service GitHub->>Middleware: POST /routes/github/webhook (with X-Hub-Signature-256) rect rgb(240, 248, 255) note right of Middleware: Step 1: Authentication Guard Middleware->>Middleware: Verify HMAC SHA-256 Signature alt Invalid Signature Middleware-->>GitHub: 403 Forbidden (Aborted) end end rect rgb(245, 245, 245) note right of Middleware: Step 2: Replay/Deduplication Guard Middleware->>DB: Query X-GitHub-Delivery ID alt Already Processed DB-->>Middleware: Duplicate Detected Middleware-->>GitHub: 200 OK (Ignored/No-op) end Middleware->>DB: Save X-GitHub-Delivery ID as 'pending' end rect rgb(255, 245, 238) note right of Handler: Step 3: Assignment & Spam Guard Middleware->>Handler: Forward Validated Payload Handler->>Handler: Validate Repo Whitelist & Collaborator Permissions Handler->>Handler: Check Assignee Concurrent Assignment Limits alt Spam / Over-assignment Detected Handler-->>GitHub: 400 Bad Request / 200 OK (Log Spam Warning) end end Handler->>DB: Update Issue/PR Status & Record Assignment Handler-->>GitHub: 200 OK (Success)2. Webhook Authentication Guard (HMAC SHA-256)
GitHub signs each webhook payload with a cryptographic hash using a pre-configured secret. We must calculate the signature of the raw incoming request body and match it with the
X-Hub-Signature-256header.Security Best Practices
hmac.compare_digestto compare signatures, preventing side-channel timing attacks.GITHUB_WEBHOOK_SECRET).Python / FastAPI Implementation
3. Idempotency Guard (Deduplication)
GitHub may occasionally retry a webhook request (e.g., due to network timeouts, connection drops, or cold startup delays in the backend), leading to duplicate issue/PR processings.
To guarantee exactly-once processing, we implement an Idempotency Guard:
X-GitHub-Deliveryheader.X-GitHub-Deliveryexists in ourprocessed_webhookstable.200 OK(to prevent GitHub from marking it as failed).processingand proceed.4. Assignment Validation & Spam Prevention Logic
Once authenticated and deduplicated, we validate the event metadata to reject bad actors, spam bots, and unauthorized changes.
A. Repository Whitelisting
To prevent cross-repository injection attacks, we only accept events from repositories that are explicitly registered on the InPactAI platform:
payload["repository"]["id"]orpayload["repository"]["full_name"]against our registered repository configurations.B. Assignment Authority Guard
In a collaborative open-source platform, users shouldn't be allowed to maliciously trigger assignment webhooks using fake setups or mock events.
payload["sender"]["login"]represents the actor who performed the assignment.admin,write, or authorizedmaintainerprivileges on that repository.C. Contributor Concurrency Limit (Spam Claiming)
A common spam pattern in GSoC/hackathons is a single user claiming 5+ issues at once to block others, then abandoning them.
D. Event Context Validation
Ensure that the issue state matches the event action:
issues.assignedevents, verify that the issue is currentlyopen.pull_requestevents, reject assignment attempts if the PR is alreadyclosedormerged.5. SQLAlchemy Database Schemas
To support these security guards, we define three tables:
processed_webhooks: Stores delivery IDs for idempotency checks.registered_repositories: Stores platform-approved repositories and their optional per-repo credentials.active_assignments: Tracks active work to enforce concurrency and claim integrity.6. End-to-End FastAPI Router Blueprint
Integrating all security layers together, here is the robust router blueprint for
/routes/github: