Skip to content

Commit 540e3a5

Browse files
Merge commit from fork
* feat: limit payload size Signed-off-by: pashakostohrys <[email protected]> * git cherry-pick a6841386468365e458a61896cc48ff18163f25c0 Signed-off-by: pashakostohrys <[email protected]> * git cherry-pick a08356a8eb13ced5850ec886f4a3b823de606f65 Signed-off-by: pashakostohrys <[email protected]> * git cherry-pick f0a1f1efe721d486d6fe6cb4f645b269c6781794 Signed-off-by: pashakostohrys <[email protected]> * git cherry-pick 3dd77292073c692f0deb7b4296976a60489bc442 Signed-off-by: pashakostohrys <[email protected]> * git cherry-pick 3bf68adf84c09c44f2c42548b8421df127d61587 Signed-off-by: pashakostohrys <[email protected]> * fix cherry-pick issues Signed-off-by: pashakostohrys <[email protected]> * fix cherry-pick issues Signed-off-by: pashakostohrys <[email protected]> --------- Signed-off-by: pashakostohrys <[email protected]>
1 parent b980386 commit 540e3a5

File tree

6 files changed

+76
-5
lines changed

6 files changed

+76
-5
lines changed

docs/operator-manual/argocd-cm.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,6 @@ data:
410410
cluster:
411411
name: some-cluster
412412
server: https://some-cluster
413+
414+
# The maximum size of the payload that can be sent to the webhook server.
415+
webhook.maxPayloadSizeMB: 1024

docs/operator-manual/webhook.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ 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.
2223
## Github
2324

2425
![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
@@ -1048,7 +1048,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
10481048

10491049
// Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them)
10501050
argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset)
1051-
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, a.RepoServerCache, a.Cache, argoDB)
1051+
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, a.RepoServerCache, a.Cache, argoDB, a.settingsMgr.GetMaxWebhookPayloadSize())
10521052

10531053
mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)
10541054

util/settings/settings.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ const (
431431
settingsWebhookAzureDevOpsUsernameKey = "webhook.azuredevops.username"
432432
// settingsWebhookAzureDevOpsPasswordKey is the key for Azure DevOps webhook password
433433
settingsWebhookAzureDevOpsPasswordKey = "webhook.azuredevops.password"
434+
// settingsWebhookMaxPayloadSize is the key for the maximum payload size for webhooks in MB
435+
settingsWebhookMaxPayloadSizeMB = "webhook.maxPayloadSizeMB"
434436
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
435437
settingsApplicationInstanceLabelKey = "application.instanceLabelKey"
436438
// settingsResourceTrackingMethodKey is the key to configure tracking method for application resources
@@ -518,6 +520,11 @@ var (
518520
}
519521
)
520522

523+
const (
524+
// default max webhook payload size is 1GB
525+
defaultMaxWebhookPayloadSize = int64(1) * 1024 * 1024 * 1024
526+
)
527+
521528
// SettingsManager holds config info for a new manager with which to access Kubernetes ConfigMaps.
522529
type SettingsManager struct {
523530
ctx context.Context
@@ -2221,3 +2228,22 @@ func (mgr *SettingsManager) GetResourceCustomLabels() ([]string, error) {
22212228
}
22222229
return []string{}, nil
22232230
}
2231+
2232+
func (mgr *SettingsManager) GetMaxWebhookPayloadSize() int64 {
2233+
argoCDCM, err := mgr.getConfigMap()
2234+
if err != nil {
2235+
return defaultMaxWebhookPayloadSize
2236+
}
2237+
2238+
if argoCDCM.Data[settingsWebhookMaxPayloadSizeMB] == "" {
2239+
return defaultMaxWebhookPayloadSize
2240+
}
2241+
2242+
maxPayloadSizeMB, err := strconv.ParseInt(argoCDCM.Data[settingsWebhookMaxPayloadSizeMB], 10, 64)
2243+
if err != nil {
2244+
log.Warnf("Failed to parse '%s' key: %v", settingsWebhookMaxPayloadSizeMB, err)
2245+
return defaultMaxWebhookPayloadSize
2246+
}
2247+
2248+
return maxPayloadSizeMB * 1024 * 1024
2249+
}

util/webhook/webhook.go

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

44+
const payloadQueueSize = 50000
45+
4446
var (
4547
_ settingsSource = &settings.SettingsManager{}
4648
errBasicAuthVerificationFailed = errors.New("basic auth verification failed")
@@ -61,9 +63,11 @@ type ArgoCDWebhookHandler struct {
6163
azuredevopsAuthHandler func(r *http.Request) error
6264
gogs *gogs.Webhook
6365
settingsSrc settingsSource
66+
queue chan interface{}
67+
maxWebhookPayloadSizeB int64
6468
}
6569

66-
func NewHandler(namespace string, applicationNamespaces []string, appClientset appclientset.Interface, set *settings.ArgoCDSettings, settingsSrc settingsSource, repoCache *cache.Cache, serverCache *servercache.Cache, argoDB db.ArgoDB) *ArgoCDWebhookHandler {
70+
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 {
6771
githubWebhook, err := github.New(github.Options.Secret(set.WebhookGitHubSecret))
6872
if err != nil {
6973
log.Warnf("Unable to init the GitHub webhook")
@@ -113,6 +117,8 @@ func NewHandler(namespace string, applicationNamespaces []string, appClientset a
113117
repoCache: repoCache,
114118
serverCache: serverCache,
115119
db: argoDB,
120+
queue: make(chan interface{}, payloadQueueSize),
121+
maxWebhookPayloadSizeB: maxWebhookPayloadSizeB,
116122
}
117123

118124
return &acdWebhook
@@ -388,6 +394,8 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
388394
var payload interface{}
389395
var err error
390396

397+
r.Body = http.MaxBytesReader(w, r.Body, a.maxWebhookPayloadSizeB)
398+
391399
switch {
392400
case r.Header.Get("X-Vss-Activityid") != "":
393401
if err = a.azuredevopsAuthHandler(r); err != nil {
@@ -430,6 +438,14 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
430438
}
431439

432440
if err != nil {
441+
// If the error is due to a large payload, return a more user-friendly error message
442+
if err.Error() == "error parsing payload" {
443+
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)
444+
log.WithField(common.SecurityField, common.SecurityHigh).Warn(msg)
445+
http.Error(w, msg, http.StatusBadRequest)
446+
return
447+
}
448+
433449
log.Infof("Webhook processing failed: %s", err)
434450
status := http.StatusBadRequest
435451
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]
@@ -72,7 +78,7 @@ func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects
7278
1*time.Minute,
7379
1*time.Minute,
7480
10*time.Second,
75-
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{})
81+
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{}, maxPayloadSize)
7682
}
7783

7884
func TestGitHubCommitEvent(t *testing.T) {
@@ -392,8 +398,9 @@ func TestInvalidEvent(t *testing.T) {
392398
req.Header.Set("X-GitHub-Event", "push")
393399
w := httptest.NewRecorder()
394400
h.Handler(w, req)
395-
assert.Equal(t, w.Code, http.StatusBadRequest)
396-
expectedLogResult := "Webhook processing failed: error parsing payload"
401+
close(h.queue)
402+
assert.Equal(t, http.StatusBadRequest, w.Code)
403+
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"
397404
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
398405
assert.Equal(t, expectedLogResult+"\n", w.Body.String())
399406
hook.Reset()
@@ -604,3 +611,21 @@ func Test_getWebUrlRegex(t *testing.T) {
604611
})
605612
}
606613
}
614+
615+
func TestGitHubCommitEventMaxPayloadSize(t *testing.T) {
616+
hook := test.NewGlobal()
617+
maxPayloadSize := int64(100)
618+
h := NewMockHandlerWithPayloadLimit(nil, []string{}, maxPayloadSize)
619+
req := httptest.NewRequest(http.MethodPost, "/api/webhook", nil)
620+
req.Header.Set("X-GitHub-Event", "push")
621+
eventJSON, err := os.ReadFile("testdata/github-commit-event.json")
622+
require.NoError(t, err)
623+
req.Body = io.NopCloser(bytes.NewReader(eventJSON))
624+
w := httptest.NewRecorder()
625+
h.Handler(w, req)
626+
close(h.queue)
627+
assert.Equal(t, http.StatusBadRequest, w.Code)
628+
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"
629+
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
630+
hook.Reset()
631+
}

0 commit comments

Comments
 (0)