Skip to content

Commit 810c4f9

Browse files
authored
webhook: use regexp.QuoteMeta for hostname and path (#5145)
1 parent a6f1fc4 commit 810c4f9

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

pkg/webhook/webhook.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,8 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
105105
w.logAndReturn(rw, err)
106106
return
107107
}
108-
109-
path := strings.Replace(u.EscapedPath()[1:], "/_git/", "(/_git)?/", 1)
110-
111-
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?|git@(ssh\.)?)` + u.Hostname() +
108+
path := strings.Replace(regexp.QuoteMeta(u.EscapedPath()[1:]), `/_git/`, `(/_git)?/`, 1)
109+
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?|git@(ssh\.)?)` + regexp.QuoteMeta(u.Hostname()) +
112110
"(:[0-9]+|)[:/](v\\d/)?" + path + "(\\.git)?$"
113111
repoRegexp, err := regexp.Compile(regexpStr)
114112
if err != nil {
@@ -212,7 +210,7 @@ func HandleHooks(ctx context.Context, namespace string, client client.Client, cl
212210
func (w *Webhook) logAndReturn(rw http.ResponseWriter, err error) {
213211
w.log.Error(err, "Webhook processing failed")
214212
rw.WriteHeader(getErrorCodeFromErr(err))
215-
_, _ = rw.Write([]byte(err.Error()))
213+
_, _ = rw.Write([]byte(err.Error())) //nolint:gosec // G705: error message is not user-controlled
216214
}
217215

218216
func (w *Webhook) getSecret(ctx context.Context, gitrepo fleet.GitRepo) (*corev1.Secret, error) {

pkg/webhook/webhook_test.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ func TestErrorReadingRequest(t *testing.T) {
874874
client: mockClient,
875875
namespace: "default",
876876
}
877-
testRequest := httptest.NewRequest(http.MethodPost, "/something", errReader(0))
877+
testRequest := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/something", errReader(0))
878878
rr := httptest.NewRecorder()
879879
w.ServeHTTP(rr, testRequest)
880880

@@ -883,3 +883,83 @@ func TestErrorReadingRequest(t *testing.T) {
883883
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
884884
}
885885
}
886+
887+
// TestGitHubWildcardURLDoesNotMatchUnintendedRepo verifies that regex metacharacters
888+
// in a webhook payload's repo URL (e.g. a hostname or path of ".*") are treated as
889+
// literals and do not accidentally match GitRepos whose URLs are unrelated to the
890+
// webhook source.
891+
func TestGitHubWildcardURLDoesNotMatchUnintendedRepo(t *testing.T) {
892+
tests := []struct {
893+
name string
894+
gitRepoURL string // the GitRepo's configured repo
895+
webhookRepoURL string // the repo URL embedded in the webhook payload
896+
}{
897+
{
898+
name: "wildcard hostname does not match unrelated gitrepo",
899+
gitRepoURL: "https://github.com/example/repo",
900+
webhookRepoURL: "https://.*/example/repo",
901+
},
902+
{
903+
name: "wildcard path does not match unrelated gitrepo",
904+
gitRepoURL: "https://github.com/example/repo",
905+
webhookRepoURL: "https://github.com/.*/.*",
906+
},
907+
{
908+
name: "wildcard hostname and path do not match unrelated gitrepo",
909+
gitRepoURL: "https://github.com/example/repo",
910+
webhookRepoURL: "https://.*/.*",
911+
},
912+
}
913+
914+
for _, tt := range tests {
915+
t.Run(tt.name, func(t *testing.T) {
916+
sch := runtime.NewScheme()
917+
if err := v1alpha1.AddToScheme(sch); err != nil {
918+
t.Fatalf("unable to add to scheme: %v", err)
919+
}
920+
utilruntime.Must(corev1.AddToScheme(sch))
921+
922+
gitRepo := &v1alpha1.GitRepo{
923+
ObjectMeta: metav1.ObjectMeta{
924+
Name: "test",
925+
Namespace: "default",
926+
},
927+
Spec: v1alpha1.GitRepoSpec{
928+
Repo: tt.gitRepoURL,
929+
Branch: "main",
930+
},
931+
}
932+
fakeClient := cfake.NewClientBuilder().
933+
WithScheme(sch).
934+
WithRuntimeObjects(gitRepo).
935+
WithStatusSubresource(gitRepo).
936+
Build()
937+
938+
w := &Webhook{client: fakeClient, namespace: "default"}
939+
940+
commit := "af69d162de5a276abc86e0686b2b44033cd3f442"
941+
jsonBody := fmt.Appendf(nil, `{
942+
"ref": "refs/heads/main",
943+
"after": "%s",
944+
"repository": {"html_url": "%s"}
945+
}`, commit, tt.webhookRepoURL)
946+
947+
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/", bytes.NewReader(jsonBody))
948+
if err != nil {
949+
t.Fatalf("failed to create request: %v", err)
950+
}
951+
req.Header.Set("X-GitHub-Event", "push")
952+
953+
rr := httptest.NewRecorder()
954+
w.ServeHTTP(rr, req)
955+
956+
updated := &v1alpha1.GitRepo{}
957+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, updated); err != nil {
958+
t.Fatalf("failed to get gitrepo: %v", err)
959+
}
960+
if updated.Status.WebhookCommit != "" {
961+
t.Errorf("expected WebhookCommit to remain empty (no match), but got %q", updated.Status.WebhookCommit)
962+
}
963+
})
964+
}
965+
}

0 commit comments

Comments
 (0)