From 58595330f410c815d89276f5f2a8d53b1642390f Mon Sep 17 00:00:00 2001 From: ex-takashima Date: Thu, 12 Mar 2026 16:33:18 +0900 Subject: [PATCH] fix(line): limit webhook request body size to prevent DoS Add io.LimitReader with 1 MB cap on the LINE webhook handler to prevent unauthenticated memory exhaustion via oversized POST requests. Follows the same pattern used in the WeCom channel (io.LimitReader). Requests exceeding the limit are rejected with 413 Request Entity Too Large. Fixes #1407 Co-Authored-By: Claude Opus 4.6 --- pkg/channels/line/line.go | 10 ++++++- pkg/channels/line/line_test.go | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 pkg/channels/line/line_test.go diff --git a/pkg/channels/line/line.go b/pkg/channels/line/line.go index b36350a06..c60c0ee1c 100644 --- a/pkg/channels/line/line.go +++ b/pkg/channels/line/line.go @@ -166,7 +166,10 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) { return } - body, err := io.ReadAll(r.Body) + // Limit request body to prevent memory exhaustion (DoS). + // LINE webhook payloads are typically a few KB; 1 MB is generous. + const maxWebhookBodySize = 1 << 20 // 1 MB + body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookBodySize+1)) if err != nil { logger.ErrorCF("line", "Failed to read request body", map[string]any{ "error": err.Error(), @@ -174,6 +177,11 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad request", http.StatusBadRequest) return } + if int64(len(body)) > maxWebhookBodySize { + logger.WarnC("line", "Webhook request body too large, rejected") + http.Error(w, "Request entity too large", http.StatusRequestEntityTooLarge) + return + } signature := r.Header.Get("X-Line-Signature") if !c.verifySignature(body, signature) { diff --git a/pkg/channels/line/line_test.go b/pkg/channels/line/line_test.go new file mode 100644 index 000000000..b8052c471 --- /dev/null +++ b/pkg/channels/line/line_test.go @@ -0,0 +1,52 @@ +package line + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestWebhookRejectsOversizedBody(t *testing.T) { + ch := &LINEChannel{} + + // Create a body larger than maxWebhookBodySize (1 MB) + oversized := bytes.Repeat([]byte("A"), (1<<20)+1) + req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) + rec := httptest.NewRecorder() + + ch.webhookHandler(rec, req) + + if rec.Code != http.StatusRequestEntityTooLarge { + t.Errorf("expected status %d, got %d", http.StatusRequestEntityTooLarge, rec.Code) + } +} + +func TestWebhookRejectsNonPostMethod(t *testing.T) { + ch := &LINEChannel{} + + req := httptest.NewRequest(http.MethodGet, "/webhook", nil) + rec := httptest.NewRecorder() + + ch.webhookHandler(rec, req) + + if rec.Code != http.StatusMethodNotAllowed { + t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code) + } +} + +func TestWebhookRejectsInvalidSignature(t *testing.T) { + ch := &LINEChannel{} + + body := `{"events":[]}` + req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body)) + req.Header.Set("X-Line-Signature", "invalidsignature") + rec := httptest.NewRecorder() + + ch.webhookHandler(rec, req) + + if rec.Code != http.StatusForbidden { + t.Errorf("expected status %d, got %d", http.StatusForbidden, rec.Code) + } +}