From 8024a09e670c4841c7df30881fff1b3f9d6e67f7 Mon Sep 17 00:00:00 2001 From: horsley Date: Thu, 12 Mar 2026 11:54:21 +0000 Subject: [PATCH] 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. --- pkg/channels/matrix/matrix.go | 50 ++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/pkg/channels/matrix/matrix.go b/pkg/channels/matrix/matrix.go index a45207f12..5069ac553 100644 --- a/pkg/channels/matrix/matrix.go +++ b/pkg/channels/matrix/matrix.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "html" + "io" "mime" + "net/http" "net/url" "os" "path/filepath" @@ -726,19 +728,6 @@ func (c *MatrixChannel) downloadMedia( reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second) 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) ext := matrixMediaExt(label, matrixContentType(msgEvt), mediaKind) mediaDir, err := matrixMediaTempDir() @@ -751,9 +740,38 @@ func (c *MatrixChannel) downloadMedia( } defer tmp.Close() - if _, err = tmp.Write(data); err != nil { - _ = os.Remove(tmp.Name()) - return "", err + // 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 { + _ = os.Remove(tmp.Name()) + 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