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 <noreply@anthropic.com>
This commit is contained in:
ex-takashima 2026-03-12 16:33:18 +09:00
parent b5bd434ddb
commit 58595330f4
2 changed files with 61 additions and 1 deletions

View file

@ -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) {

View file

@ -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)
}
}