Skip to content

Commit d881ee7

Browse files
Merge commit from fork
* feat: limit payload size Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix cherry-pick issues Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix cherry-pick issues Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix cherry-pick issues Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix cherry-pick issues Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix cherry-pick issues Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix linter Signed-off-by: pashakostohrys <pavel@codefresh.io> * fix lint and test issues Signed-off-by: pashakostohrys <pavel@codefresh.io> --------- Signed-off-by: pashakostohrys <pavel@codefresh.io>
1 parent 8e81818 commit d881ee7

File tree

6 files changed

+80
-11
lines changed

6 files changed

+80
-11
lines changed

docs/operator-manual/argocd-cm.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,5 @@ data:
406406
cluster:
407407
name: some-cluster
408408
server: https://some-cluster
409+
# The maximum size of the payload that can be sent to the webhook server.
410+
webhook.maxPayloadSizeMB: 1024

docs/operator-manual/webhook.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ URL configured in the Git provider should use the `/api/webhook` endpoint of you
1919
(e.g. `https://argocd.example.com/api/webhook`). If you wish to use a shared secret, input an
2020
arbitrary value in the secret. This value will be used when configuring the webhook in the next step.
2121

22+
To prevent DDoS attacks with unauthenticated webhook events (the `/api/webhook` endpoint currently lacks rate limiting protection), it is recommended to limit the payload size. You can achieve this by configuring the `argocd-cm` ConfigMap with the `webhook.maxPayloadSizeMB` attribute. The default value is 1GB.
23+
2224
## Github
2325

2426
![Add Webhook](../assets/webhook-config.png "Add Webhook")

server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
10341034

10351035
// Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them)
10361036
argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset)
1037-
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour, 3*time.Minute), a.Cache, argoDB)
1037+
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour, 3*time.Minute), a.Cache, argoDB, a.settingsMgr.GetMaxWebhookPayloadSize())
10381038

10391039
mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)
10401040

util/settings/settings.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ const (
420420
settingsWebhookAzureDevOpsUsernameKey = "webhook.azuredevops.username"
421421
// settingsWebhookAzureDevOpsPasswordKey is the key for Azure DevOps webhook password
422422
settingsWebhookAzureDevOpsPasswordKey = "webhook.azuredevops.password"
423+
// settingsWebhookMaxPayloadSize is the key for the maximum payload size for webhooks in MB
424+
settingsWebhookMaxPayloadSizeMB = "webhook.maxPayloadSizeMB"
423425
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
424426
settingsApplicationInstanceLabelKey = "application.instanceLabelKey"
425427
// settingsResourceTrackingMethodKey is the key to configure tracking method for application resources
@@ -497,14 +499,17 @@ const (
497499
RespectRBACValueNormal = "normal"
498500
)
499501

500-
var (
501-
sourceTypeToEnableGenerationKey = map[v1alpha1.ApplicationSourceType]string{
502-
v1alpha1.ApplicationSourceTypeKustomize: "kustomize.enable",
503-
v1alpha1.ApplicationSourceTypeHelm: "helm.enable",
504-
v1alpha1.ApplicationSourceTypeDirectory: "jsonnet.enable",
505-
}
502+
const (
503+
// default max webhook payload size is 1GB
504+
defaultMaxWebhookPayloadSize = int64(1) * 1024 * 1024 * 1024
506505
)
507506

507+
var sourceTypeToEnableGenerationKey = map[v1alpha1.ApplicationSourceType]string{
508+
v1alpha1.ApplicationSourceTypeKustomize: "kustomize.enable",
509+
v1alpha1.ApplicationSourceTypeHelm: "helm.enable",
510+
v1alpha1.ApplicationSourceTypeDirectory: "jsonnet.enable",
511+
}
512+
508513
// SettingsManager holds config info for a new manager with which to access Kubernetes ConfigMaps.
509514
type SettingsManager struct {
510515
ctx context.Context
@@ -2159,3 +2164,22 @@ func (mgr *SettingsManager) GetResourceCustomLabels() ([]string, error) {
21592164
}
21602165
return []string{}, nil
21612166
}
2167+
2168+
func (mgr *SettingsManager) GetMaxWebhookPayloadSize() int64 {
2169+
argoCDCM, err := mgr.getConfigMap()
2170+
if err != nil {
2171+
return defaultMaxWebhookPayloadSize
2172+
}
2173+
2174+
if argoCDCM.Data[settingsWebhookMaxPayloadSizeMB] == "" {
2175+
return defaultMaxWebhookPayloadSize
2176+
}
2177+
2178+
maxPayloadSizeMB, err := strconv.ParseInt(argoCDCM.Data[settingsWebhookMaxPayloadSizeMB], 10, 64)
2179+
if err != nil {
2180+
log.Warnf("Failed to parse '%s' key: %v", settingsWebhookMaxPayloadSizeMB, err)
2181+
return defaultMaxWebhookPayloadSize
2182+
}
2183+
2184+
return maxPayloadSizeMB * 1024 * 1024
2185+
}

util/webhook/webhook.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type settingsSource interface {
4242
// https://github.com/shadow-maint/shadow/blob/master/libmisc/chkname.c#L36
4343
const usernameRegex = `[a-zA-Z0-9_\.][a-zA-Z0-9_\.-]{0,30}[a-zA-Z0-9_\.\$-]?`
4444

45+
const payloadQueueSize = 50000
46+
4547
var (
4648
_ settingsSource = &settings.SettingsManager{}
4749
errBasicAuthVerificationFailed = errors.New("basic auth verification failed")
@@ -62,9 +64,11 @@ type ArgoCDWebhookHandler struct {
6264
azuredevopsAuthHandler func(r *http.Request) error
6365
gogs *gogs.Webhook
6466
settingsSrc settingsSource
67+
queue chan interface{}
68+
maxWebhookPayloadSizeB int64
6569
}
6670

67-
func NewHandler(namespace string, applicationNamespaces []string, appClientset appclientset.Interface, set *settings.ArgoCDSettings, settingsSrc settingsSource, repoCache *cache.Cache, serverCache *servercache.Cache, argoDB db.ArgoDB) *ArgoCDWebhookHandler {
71+
func NewHandler(namespace string, applicationNamespaces []string, appClientset appclientset.Interface, set *settings.ArgoCDSettings, settingsSrc settingsSource, repoCache *cache.Cache, serverCache *servercache.Cache, argoDB db.ArgoDB, maxWebhookPayloadSizeB int64) *ArgoCDWebhookHandler {
6872
githubWebhook, err := github.New(github.Options.Secret(set.WebhookGitHubSecret))
6973
if err != nil {
7074
log.Warnf("Unable to init the GitHub webhook")
@@ -114,6 +118,8 @@ func NewHandler(namespace string, applicationNamespaces []string, appClientset a
114118
repoCache: repoCache,
115119
serverCache: serverCache,
116120
db: argoDB,
121+
queue: make(chan interface{}, payloadQueueSize),
122+
maxWebhookPayloadSizeB: maxWebhookPayloadSizeB,
117123
}
118124

119125
return &acdWebhook
@@ -458,6 +464,8 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
458464
var payload interface{}
459465
var err error
460466

467+
r.Body = http.MaxBytesReader(w, r.Body, a.maxWebhookPayloadSizeB)
468+
461469
switch {
462470
case r.Header.Get("X-Vss-Activityid") != "":
463471
if err = a.azuredevopsAuthHandler(r); err != nil {
@@ -500,6 +508,14 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
500508
}
501509

502510
if err != nil {
511+
// If the error is due to a large payload, return a more user-friendly error message
512+
if err.Error() == "error parsing payload" {
513+
msg := fmt.Sprintf("Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under %v MB) and ensure it is valid JSON", a.maxWebhookPayloadSizeB/1024/1024)
514+
log.WithField(common.SecurityField, common.SecurityHigh).Warn(msg)
515+
http.Error(w, msg, http.StatusBadRequest)
516+
return
517+
}
518+
503519
log.Infof("Webhook processing failed: %s", err)
504520
status := http.StatusBadRequest
505521
if r.Method != http.MethodPost {

util/webhook/webhook_test.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"github.com/stretchr/testify/require"
78
"io"
89
"net/http"
910
"net/http/httptest"
@@ -56,6 +57,11 @@ type reactorDef struct {
5657
}
5758

5859
func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects ...runtime.Object) *ArgoCDWebhookHandler {
60+
defaultMaxPayloadSize := int64(1) * 1024 * 1024 * 1024
61+
return NewMockHandlerWithPayloadLimit(reactor, applicationNamespaces, defaultMaxPayloadSize, objects...)
62+
}
63+
64+
func NewMockHandlerWithPayloadLimit(reactor *reactorDef, applicationNamespaces []string, maxPayloadSize int64, objects ...runtime.Object) *ArgoCDWebhookHandler {
5965
appClientset := appclientset.NewSimpleClientset(objects...)
6066
if reactor != nil {
6167
defaultReactor := appClientset.ReactionChain[0]
@@ -71,7 +77,7 @@ func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects
7177
cacheClient,
7278
1*time.Minute,
7379
1*time.Minute,
74-
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{})
80+
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{}, maxPayloadSize)
7581
}
7682

7783
func TestGitHubCommitEvent(t *testing.T) {
@@ -391,8 +397,9 @@ func TestInvalidEvent(t *testing.T) {
391397
req.Header.Set("X-GitHub-Event", "push")
392398
w := httptest.NewRecorder()
393399
h.Handler(w, req)
394-
assert.Equal(t, w.Code, http.StatusBadRequest)
395-
expectedLogResult := "Webhook processing failed: error parsing payload"
400+
close(h.queue)
401+
assert.Equal(t, http.StatusBadRequest, w.Code)
402+
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 1024 MB) and ensure it is valid JSON"
396403
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
397404
assert.Equal(t, expectedLogResult+"\n", w.Body.String())
398405
hook.Reset()
@@ -683,3 +690,21 @@ func Test_getWebUrlRegex(t *testing.T) {
683690
})
684691
}
685692
}
693+
694+
func TestGitHubCommitEventMaxPayloadSize(t *testing.T) {
695+
hook := test.NewGlobal()
696+
maxPayloadSize := int64(100)
697+
h := NewMockHandlerWithPayloadLimit(nil, []string{}, maxPayloadSize)
698+
req := httptest.NewRequest(http.MethodPost, "/api/webhook", nil)
699+
req.Header.Set("X-GitHub-Event", "push")
700+
eventJSON, err := os.ReadFile("testdata/github-commit-event.json")
701+
require.NoError(t, err)
702+
req.Body = io.NopCloser(bytes.NewReader(eventJSON))
703+
w := httptest.NewRecorder()
704+
h.Handler(w, req)
705+
close(h.queue)
706+
assert.Equal(t, http.StatusBadRequest, w.Code)
707+
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 0 MB) and ensure it is valid JSON"
708+
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
709+
hook.Reset()
710+
}

0 commit comments

Comments
 (0)