matrix: use streaming download for non-encrypted media

Use mautrix.client.Download() with io.Copy for non-encrypted files
to avoid loading entire file into memory. This prevents potential
memory exhaustion DoS attacks via large attachments.

Encrypted files still use DownloadBytes as they require in-memory
decryption.
This commit is contained in:
horsley 2026-03-12 11:54:21 +00:00
parent d18a319b0c
commit 8024a09e67

View file

@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"html" "html"
"io"
"mime" "mime"
"net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -726,19 +728,6 @@ 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)
if err != nil {
return "", err
}
// Encrypted attachments put URL in msgEvt.File and require client-side decryption.
if msgEvt != nil && msgEvt.File != nil && msgEvt.URL == "" {
err = msgEvt.File.DecryptInPlace(data)
if err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err)
}
}
label := matrixMediaLabel(msgEvt, mediaKind) label := matrixMediaLabel(msgEvt, mediaKind)
ext := matrixMediaExt(label, matrixContentType(msgEvt), mediaKind) ext := matrixMediaExt(label, matrixContentType(msgEvt), mediaKind)
mediaDir, err := matrixMediaTempDir() mediaDir, err := matrixMediaTempDir()
@ -751,10 +740,39 @@ func (c *MatrixChannel) downloadMedia(
} }
defer tmp.Close() defer tmp.Close()
// Encrypted attachments require client-side decryption, must read fully into memory.
// For encrypted files, fall back to DownloadBytes.
isEncrypted := msgEvt != nil && msgEvt.File != nil && msgEvt.URL == ""
if isEncrypted {
data, err := c.client.DownloadBytes(reqCtx, parsed)
if err != nil {
return "", err
}
err = msgEvt.File.DecryptInPlace(data)
if err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err)
}
if _, err = tmp.Write(data); err != nil { if _, err = tmp.Write(data); err != nil {
_ = os.Remove(tmp.Name()) _ = os.Remove(tmp.Name())
return "", err return "", err
} }
} else {
// Use streaming download for non-encrypted files to avoid loading entire file into memory.
resp, err := c.client.Download(reqCtx, parsed)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("download failed with status: %d", resp.StatusCode)
}
if _, err := io.Copy(tmp, resp.Body); err != nil {
_ = os.Remove(tmp.Name())
return "", err
}
}
return tmp.Name(), nil return tmp.Name(), nil
} }