fix(security): cap webhook + config PATCH bodies (H3/H4)

Two HIGH-severity DoS surfaces: both handlers read the entire HTTP
body with io.ReadAll(r.Body) and no upper bound, so a caller streaming
a multi-gigabyte request could exhaust memory on the tenant instance
before we even validated the JSON.

H3 (Discord webhook): wrap Body in io.LimitReader with a 1 MiB cap.
Discord Interactions payloads are well under 10 KiB in practice.

H4 (workspace config PATCH): wrap Body in http.MaxBytesReader with a
256 KiB cap. Real configs are <10 KiB; jsonb handles the cap
comfortably. Returns 413 Request Entity Too Large on overflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-19 01:23:03 -07:00
parent b367f18e95
commit 60c4801a13
2 changed files with 11 additions and 2 deletions

View File

@ -106,7 +106,11 @@ func (d *DiscordAdapter) SendMessage(ctx context.Context, config map[string]inte
// Returns nil, nil for PING payloads — the handler layer must respond with `{"type":1}` to pass
// Discord's endpoint verification. Returns an InboundMessage for APPLICATION_COMMAND payloads.
func (d *DiscordAdapter) ParseWebhook(c *gin.Context, _ map[string]interface{}) (*InboundMessage, error) {
body, err := io.ReadAll(c.Request.Body)
// Cap incoming webhook bodies at 1 MiB. Discord's Interactions API
// payloads are well under 10 KiB in practice; the cap is a DoS
// guard, not a functional limit.
const maxDiscordWebhook = 1 << 20
body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxDiscordWebhook))
if err != nil {
return nil, fmt.Errorf("discord: read body: %w", err)
}

View File

@ -42,9 +42,14 @@ func (h *ConfigHandler) Get(c *gin.Context) {
func (h *ConfigHandler) Patch(c *gin.Context) {
workspaceID := c.Param("id")
// 256 KiB cap: Postgres jsonb comfortably handles this and real
// configs are <10 KiB. The cap blocks naive memory-exhaustion DoS
// — a caller streaming a gigabyte of JSON would OOM the instance.
const maxConfigBody = 256 << 10
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxConfigBody)
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "body too large or unreadable"})
return
}