fix: gracefully skip invalid repositories in webhooks instead of aborting#6360
fix: gracefully skip invalid repositories in webhooks instead of aborting#6360AftAb-25 wants to merge 2 commits intomindersec:mainfrom
Conversation
0b41aa4 to
b58a74b
Compare
b58a74b to
2fca364
Compare
Do you have evidence that any of these actually happen, or is this hypothetical on bad data from GitHub? I'm happy to harden Minder against bad GitHub data, but given that the payload is delivered via HTTPS with a signature from GitHub, it seems a bit of a stretch to claim this is a "massive vulnerability" causing "data starvation and ghost-access leaks". In particular, if Minder misses notification that a repository was removed from the app installation, GitHub should still enforce the access control and prevent Minder from accessing that repository. (Minder may churn extra on the API call failures, but this shouldn't stop Minder from processing other repositories.) |
| func strPtr(s string) *string { return &s } | ||
| func int64Ptr(i int64) *int64 { return &i } |
There was a problem hiding this comment.
Consider using the generic ptr.Ptr() from internal/util/ptr rather than defining your own.
| validRepo := func(id int, name, fullName string) *repo { | ||
| idVal := int64(id) | ||
| return &repo{ | ||
| ID: &idVal, | ||
| Name: &name, | ||
| FullName: &fullName, | ||
| } | ||
| } | ||
|
|
||
| invalidRepo := func() *repo { | ||
| // name is empty, triggers repositoryAdded validation error | ||
| emptyName := "" | ||
| return &repo{ | ||
| Name: &emptyName, | ||
| } | ||
| } | ||
|
|
||
| zeroIDRepo := func() *repo { | ||
| // ID is 0, triggers repositoryRemoved validation error | ||
| var zero int64 | ||
| name := "bad-repo" | ||
| return &repo{ | ||
| ID: &zero, | ||
| Name: &name, | ||
| } | ||
| } | ||
|
|
||
| mockInstallation := db.ProviderGithubAppInstallation{ | ||
| ProjectID: uuid.NullUUID{UUID: projectID, Valid: true}, | ||
| ProviderID: uuid.NullUUID{UUID: providerID, Valid: true}, | ||
| } | ||
|
|
||
| baseMocks := func(ctrl *gomock.Controller) db.Store { | ||
| return df.NewMockStore( | ||
| df.WithSuccessfulGetInstallationIDByAppID(mockInstallation, 54321), | ||
| df.WithSuccessfulGetProviderByID( | ||
| db.Provider{ | ||
| ID: providerID, | ||
| Definition: json.RawMessage(autoregEnabled), | ||
| }, | ||
| providerID, | ||
| ), | ||
| )(ctrl) | ||
| } |
There was a problem hiding this comment.
We don't generally define private functions like this in tests; declare it as a pure function below the test framework if you need to modularize it.
There was a problem hiding this comment.
Just call this file app_test.go. Alternatively, consider extending the existing test cases in internal/providers/github/webhook/handlers_githubwebhooks_test.go, which include cases for garbage content, but not 0 repository IDs.
| autoregEnabled := `{"github-app": {}, "auto_registration": {"entities": {"repository": {"enabled": true}}}}` | ||
|
|
||
| validRepo := func(id int, name, fullName string) *repo { | ||
| idVal := int64(id) |
There was a problem hiding this comment.
Why not take an int64 argument here?
| invalidRepo := func() *repo { | ||
| // name is empty, triggers repositoryAdded validation error | ||
| emptyName := "" | ||
| return &repo{ | ||
| Name: &emptyName, | ||
| } | ||
| } |
There was a problem hiding this comment.
This and the next seems like the callers could re-use the validRepo function, rather than needing to create a separate function.
|
Hi @evankanderson, thank you for the detailed pushback — you're raising completely fair points and I want to address them honestly. On the severity framing: You're right that "massive vulnerability" and "ghost-access leaks" were overstatements on my part. GitHub's HTTPS signature validation means the payload is authentic, and as you correctly note, GitHub's own access control layer still enforces revocation even if Minder misses a On the actual observable behavior: The concrete problem is narrower than I described: if GitHub delivers a malformed or partially-corrupt
The fix is minimal defensive coding — swap Adjusted PR description: I've toned down the description to accurately reflect this as a batch resilience improvement rather than a security fix. Happy to update the PR title too if that helps. Let me know if you'd like me to make any other adjustments! |
|
@evankanderson You're 100% right to ask for evidence—this was actually a proactive finding discovered during a code audit, rather than a bug flagged from a live production incident or customer report. While reviewing the webhook processing logic, I noticed that processInstallationRepositoriesAppEvent lacked the fault tolerance we usually apply to batch processors. My concern was that if an I don't have concrete log evidence of GitHub sending malformed My goal here is simply to proactively harden the loop in the engine so that if an anomaly does occur, Minder degrades gracefully by skipping the bad entry and syncing the rest, rather than dropping the entire payload batch. |
- Rename app_batch_test.go -> app_test.go per reviewer suggestion - Use ptr.Ptr[T]() from internal/util/ptr instead of local strPtr/int64Ptr helpers - Move validRepo/invalidRepo/zeroIDRepo to top-level package functions - Change validRepo to accept int64 instead of int - Fix processInstallationRepositoriesAppEvent to skip (not abort) on invalid repos - Add zero-ID guard for removed repos in the same loop Addresses review comments from @evankanderson
Description
This PR improves batch resilience inside the GitHub App Webhook processor (processInstallationRepositoriesAppEvent) by preventing a single invalid repository entry from aborting the entire synchronization event.
When an
installation_repositoriespayload arrives from GitHub, it iterates through bothaddedReposandremovedReposto synchronize the installation. Previously, if any single repository failed the basic name or ID parsing validation (e.g. an empty name string), the function would instantly return anerror.This caused two minor but unwanted behaviors:
Changes
return nil, erracross both the added and removed loops with a gracefulzerologwarning and acontinue, allowing valid entries to continue processing.repo.GetID() != 0to safely surface errors rather than passing zero-values downstream.Fixes #6359
Checklist