Skip to content

Commit 321c9ed

Browse files
committed
feat: support POST body for incoming webhook secrets, warn on legacy URL method
1 parent b2a2238 commit 321c9ed

File tree

2 files changed

+95
-2
lines changed

2 files changed

+95
-2
lines changed

pkg/adapter/incoming.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package adapter
33
import (
44
"context"
55
"crypto/subtle"
6+
"encoding/json"
67
"fmt"
78
"net/http"
89

@@ -50,14 +51,46 @@ func applyIncomingParams(req *http.Request, payloadBody []byte, params []string)
5051
}
5152

5253
func (l *listener) detectIncoming(ctx context.Context, req *http.Request, payloadBody []byte) (bool, *v1alpha1.Repository, error) {
54+
// Support both legacy (URL query) and new (POST body) secret passing
5355
repository := req.URL.Query().Get("repository")
54-
querySecret := req.URL.Query().Get("secret")
55-
pipelineRun := req.URL.Query().Get("pipelinerun")
5656
branch := req.URL.Query().Get("branch")
57+
pipelineRun := req.URL.Query().Get("pipelinerun")
58+
querySecret := req.URL.Query().Get("secret")
59+
legacyMode := false
5760

5861
if req.URL.Path != "/incoming" {
5962
return false, nil, nil
6063
}
64+
65+
// If not all required query params are present, try to parse from JSON body
66+
if repository == "" || branch == "" || pipelineRun == "" || querySecret == "" {
67+
if req.Method == http.MethodPost && req.Header.Get("Content-Type") == "application/json" && len(payloadBody) > 0 {
68+
var body struct {
69+
Repository string `json:"repository"`
70+
Branch string `json:"branch"`
71+
PipelineRun string `json:"pipelinerun"`
72+
Secret string `json:"secret"`
73+
Params map[string]any `json:"params"`
74+
}
75+
if err := json.Unmarshal(payloadBody, &body); err == nil {
76+
repository = body.Repository
77+
branch = body.Branch
78+
pipelineRun = body.PipelineRun
79+
querySecret = body.Secret
80+
} else {
81+
return false, nil, fmt.Errorf("invalid JSON body for incoming webhook: %w", err)
82+
}
83+
} else {
84+
return false, nil, fmt.Errorf("missing query URL argument: pipelinerun, branch, repository, secret: '%s' '%s' '%s' '%s'", pipelineRun, branch, repository, querySecret)
85+
}
86+
} else {
87+
legacyMode = true
88+
}
89+
90+
if legacyMode {
91+
l.logger.Warnf("[SECURITY] Incoming webhook used legacy URL-based secret passing. This is insecure and will be deprecated. Please use POST body instead.")
92+
}
93+
6194
l.logger.Infof("incoming request has been requested: %v", req.URL)
6295
if pipelineRun == "" || repository == "" || querySecret == "" || branch == "" {
6396
err := fmt.Errorf("missing query URL argument: pipelinerun, branch, repository, secret: '%s' '%s' '%s' '%s'", pipelineRun, branch, repository, querySecret)

pkg/adapter/incoming_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,3 +913,63 @@ func TestApplyIncomingParams(t *testing.T) {
913913
})
914914
}
915915
}
916+
917+
func Test_detectIncoming_legacy_warning(t *testing.T) {
918+
ctx, _ := rtesting.SetupFakeContext(t)
919+
testNamespace := &corev1.Namespace{
920+
ObjectMeta: metav1.ObjectMeta{
921+
Name: "pipelinesascode",
922+
},
923+
}
924+
ctx = info.StoreCurrentControllerName(ctx, "default")
925+
ctx = info.StoreNS(ctx, testNamespace.GetName())
926+
cs, _ := testclient.SeedTestData(t, ctx, testclient.Data{
927+
Repositories: []*v1alpha1.Repository{
928+
{
929+
ObjectMeta: metav1.ObjectMeta{Name: "test-good"},
930+
Spec: v1alpha1.RepositorySpec{
931+
URL: "https://matched/by/incoming",
932+
Incomings: &[]v1alpha1.Incoming{{
933+
Targets: []string{"main"},
934+
Secret: v1alpha1.Secret{Name: "good-secret"},
935+
}},
936+
GitProvider: &v1alpha1.GitProvider{Type: "github"},
937+
},
938+
},
939+
},
940+
})
941+
client := &params.Run{
942+
Clients: clients.Clients{
943+
PipelineAsCode: cs.PipelineAsCode,
944+
Kube: cs.Kube,
945+
},
946+
Info: info.Info{
947+
Controller: &info.ControllerInfo{Secret: info.DefaultPipelinesAscodeSecretName},
948+
},
949+
}
950+
observer, observedLogs := zapobserver.New(zap.InfoLevel)
951+
logger := zap.New(observer).Sugar()
952+
kint := &kubernetestint.KinterfaceTest{GetSecretResult: map[string]string{"good-secret": "verysecrete"}}
953+
954+
l := &listener{
955+
run: client,
956+
logger: logger,
957+
kint: kint,
958+
event: info.NewEvent(),
959+
}
960+
961+
req := httptest.NewRequest("POST",
962+
"http://localhost/incoming?repository=test-good&secret=verysecrete&pipelinerun=pipelinerun1&branch=main",
963+
strings.NewReader(""))
964+
got, _, err := l.detectIncoming(ctx, req, nil)
965+
assert.NilError(t, err)
966+
assert.Assert(t, got)
967+
found := false
968+
for _, entry := range observedLogs.All() {
969+
if strings.Contains(entry.Message, "[SECURITY] Incoming webhook used legacy URL-based secret passing") {
970+
found = true
971+
break
972+
}
973+
}
974+
assert.Assert(t, found, "expected security warning log for legacy URL-based secret passing")
975+
}

0 commit comments

Comments
 (0)