mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(line): limit webhook request body size to prevent DoS (#1413)
* 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> * refactor(line): hoist body size const, add boundary tests - Move maxWebhookBodySize to package-level const - Add TestWebhookAcceptsMaxBodySize (exact limit → 403, not 413) - Add TestWebhookRejectsOversizedBodyBeforeSignatureCheck - Use const in test instead of magic number Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,10 @@ const (
|
||||
lineBotInfoEndpoint = lineAPIBase + "/info"
|
||||
lineLoadingEndpoint = lineAPIBase + "/chat/loading/start"
|
||||
lineReplyTokenMaxAge = 25 * time.Second
|
||||
|
||||
// Limit request body to prevent memory exhaustion (DoS).
|
||||
// LINE webhook payloads are typically a few KB; 1 MiB is generous.
|
||||
maxWebhookBodySize = 1 << 20 // 1 MiB
|
||||
)
|
||||
|
||||
type replyTokenEntry struct {
|
||||
@@ -166,7 +170,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
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 +178,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) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package line
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWebhookRejectsOversizedBody(t *testing.T) {
|
||||
ch := &LINEChannel{}
|
||||
|
||||
oversized := bytes.Repeat([]byte("A"), maxWebhookBodySize+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 TestWebhookAcceptsMaxBodySize(t *testing.T) {
|
||||
ch := &LINEChannel{}
|
||||
|
||||
body := bytes.Repeat([]byte("A"), maxWebhookBodySize)
|
||||
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ch.webhookHandler(rec, req)
|
||||
|
||||
// Missing signature should be rejected, but the body size should not trigger 413.
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("expected status %d, got %d", http.StatusForbidden, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookRejectsOversizedBodyBeforeSignatureCheck(t *testing.T) {
|
||||
ch := &LINEChannel{}
|
||||
|
||||
oversized := bytes.Repeat([]byte("A"), maxWebhookBodySize+1)
|
||||
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized))
|
||||
req.Header.Set("X-Line-Signature", "invalidsignature")
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user