fix(matrix): add size limit for media downloads

Replace DownloadBytes with streaming Download to avoid loading large
files into memory. Add 10MB size limit for downloaded media files.

Fixes #1405
This commit is contained in:
曾文锋0668000834 2026-03-12 20:47:06 +08:00
parent 95716b106b
commit 252ed37ac3

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"html" "html"
"io"
"mime" "mime"
"net/url" "net/url"
"os" "os"
@ -458,7 +459,7 @@ func (c *MatrixChannel) SendPlaceholder(ctx context.Context, chatID string) (str
text := strings.TrimSpace(c.config.Placeholder.Text) text := strings.TrimSpace(c.config.Placeholder.Text)
if text == "" { if text == "" {
text = "Thinking... 💭" text = "Thinking..."
} }
resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{ resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{
@ -693,6 +694,9 @@ func (c *MatrixChannel) storeMedia(localPath string, meta media.MediaMeta, scope
return localPath return localPath
} }
// defaultMaxMediaSize is the default maximum size for downloaded media (10MB).
const defaultMaxMediaSize = 10 * 1024 * 1024
func (c *MatrixChannel) downloadMedia( func (c *MatrixChannel) downloadMedia(
ctx context.Context, ctx context.Context,
msgEvt *event.MessageEventContent, msgEvt *event.MessageEventContent,
@ -714,17 +718,16 @@ func (c *MatrixChannel) downloadMedia(
reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second) reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second)
defer cancel() defer cancel()
data, err := c.client.DownloadBytes(reqCtx, parsed) // Use streaming download instead of DownloadBytes to avoid loading large files into memory
resp, err := c.client.Download(reqCtx, parsed)
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close()
// Encrypted attachments put URL in msgEvt.File and require client-side decryption. // Check Content-Length header if available
if msgEvt != nil && msgEvt.File != nil && msgEvt.URL == "" { if resp.ContentLength > defaultMaxMediaSize {
err = msgEvt.File.DecryptInPlace(data) return "", fmt.Errorf("media file too large: %d bytes (max %d)", resp.ContentLength, defaultMaxMediaSize)
if err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err)
}
} }
label := matrixMediaLabel(msgEvt, mediaKind) label := matrixMediaLabel(msgEvt, mediaKind)
@ -739,9 +742,49 @@ func (c *MatrixChannel) downloadMedia(
} }
defer tmp.Close() defer tmp.Close()
if _, err = tmp.Write(data); err != nil { // Stream data to file with size limit
_ = os.Remove(tmp.Name()) var totalBytes int64
return "", err buf := make([]byte, 32*1024) // 32KB buffer
for {
n, err := resp.Body.Read(buf)
if n > 0 {
totalBytes += int64(n)
if totalBytes > defaultMaxMediaSize {
_ = os.Remove(tmp.Name())
return "", fmt.Errorf("media file too large (exceeded %d bytes)", defaultMaxMediaSize)
}
if _, writeErr := tmp.Write(buf[:n]); writeErr != nil {
_ = os.Remove(tmp.Name())
return "", writeErr
}
}
if err == io.EOF {
break
}
if err != nil {
_ = os.Remove(tmp.Name())
return "", err
}
}
// Encrypted attachments put URL in msgEvt.File and require client-side decryption.
if msgEvt != nil && msgEvt.File != nil && msgEvt.URL == "" {
// For encrypted files, we need to read and decrypt in memory
data, err := os.ReadFile(tmp.Name())
if err != nil {
_ = os.Remove(tmp.Name())
return "", fmt.Errorf("read encrypted file: %w", err)
}
err = msgEvt.File.DecryptInPlace(data)
if err != nil {
_ = os.Remove(tmp.Name())
return "", fmt.Errorf("decrypt matrix media: %w", err)
}
// Write decrypted data back
if err := os.WriteFile(tmp.Name(), data, 0600); err != nil {
_ = os.Remove(tmp.Name())
return "", fmt.Errorf("write decrypted file: %w", err)
}
} }
return tmp.Name(), nil return tmp.Name(), nil