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>
This commit is contained in:
ex-takashima 2026-03-12 16:39:11 +09:00
parent 58595330f4
commit 73a656b919
2 changed files with 35 additions and 5 deletions

View file

@ -32,6 +32,10 @@ const (
lineBotInfoEndpoint = lineAPIBase + "/info" lineBotInfoEndpoint = lineAPIBase + "/info"
lineLoadingEndpoint = lineAPIBase + "/chat/loading/start" lineLoadingEndpoint = lineAPIBase + "/chat/loading/start"
lineReplyTokenMaxAge = 25 * time.Second 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 { type replyTokenEntry struct {
@ -166,9 +170,6 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 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)) body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookBodySize+1))
if err != nil { if err != nil {
logger.ErrorCF("line", "Failed to read request body", map[string]any{ logger.ErrorCF("line", "Failed to read request body", map[string]any{

View file

@ -11,8 +11,7 @@ import (
func TestWebhookRejectsOversizedBody(t *testing.T) { func TestWebhookRejectsOversizedBody(t *testing.T) {
ch := &LINEChannel{} ch := &LINEChannel{}
// Create a body larger than maxWebhookBodySize (1 MB) oversized := bytes.Repeat([]byte("A"), maxWebhookBodySize+1)
oversized := bytes.Repeat([]byte("A"), (1<<20)+1)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized))
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@ -23,6 +22,36 @@ func TestWebhookRejectsOversizedBody(t *testing.T) {
} }
} }
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) { func TestWebhookRejectsNonPostMethod(t *testing.T) {
ch := &LINEChannel{} ch := &LINEChannel{}