security: cap media transfer sizes

This commit is contained in:
Peter Steinberger 2026-05-04 08:30:36 +01:00
parent 515cd43b9f
commit 83d89da341
No known key found for this signature in database
7 changed files with 72 additions and 7 deletions

View File

@ -22,6 +22,7 @@
### Security
- Auth: reject `?` and `#` in whatsmeow session store paths to avoid SQLite URI parameter injection. (#180 — thanks @shaun0927)
- Media: reject send-file uploads and media downloads larger than 100 MiB before reading or writing the payload. (#63 — thanks @alexander-morris)
- Send: validate phone-number recipients before constructing WhatsApp JIDs. (#144 — thanks @draix)
- Store: restrict index and session SQLite database files to owner-only permissions. (#147 — thanks @draix)

View File

@ -20,7 +20,7 @@ Core implementation is in place. See `docs/spec.md` for design notes.
- **Offline message store**: local SQLite store with FTS5 search when available and LIKE fallback.
- **Message tools**: list/search/show/context with chat, sender, direction, time, order, and media-type filters.
- **Sending**: send text, quoted replies, and image/video/audio/document files with captions, MIME override, and custom display filenames.
- **Media**: download synced message media on demand, or download in the background during auth/sync.
- **Media**: download synced message media on demand, or download in the background during auth/sync; send-file uploads and downloads are capped at 100 MiB.
- **Contacts/chats/groups**: search/show contacts, local aliases/tags, list/show chats, refresh/list/info/rename groups, manage participants, invite links, join, and leave; left groups are hidden after leave.
- **Presence**: send typing/paused indicators.
- **Diagnostics + safety**: `doctor`, read-only mode, store locks with lock-owner reporting, lock waiting, owner-only database permissions, panic recovery, reconnect bounds, and bounded media queue backpressure.

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"fmt"
"mime"
"net/http"
"os"
@ -17,11 +18,13 @@ import (
"google.golang.org/protobuf/proto"
)
const maxSendFileSize = 100 * 1024 * 1024
func sendFile(ctx context.Context, a interface {
WA() app.WAClient
DB() *store.DB
}, to types.JID, filePath, filename, caption, mimeOverride, replyTo, replyToSender string) (string, map[string]string, error) {
data, err := os.ReadFile(filePath)
data, err := readSendFileData(filePath)
if err != nil {
return "", nil, err
}
@ -143,6 +146,17 @@ func sendFile(ctx context.Context, a interface {
}, nil
}
func readSendFileData(filePath string) ([]byte, error) {
info, err := os.Stat(filePath)
if err != nil {
return nil, err
}
if info.Size() > maxSendFileSize {
return nil, fmt.Errorf("file too large (%d bytes); maximum send file size is %d bytes", info.Size(), maxSendFileSize)
}
return os.ReadFile(filePath)
}
func attachSendFileReplyContext(msg *waProto.Message, info *waProto.ContextInfo) {
if info == nil {
return

View File

@ -1,6 +1,8 @@
package main
import (
"os"
"strings"
"testing"
waProto "go.mau.fi/whatsmeow/binary/proto"
@ -28,6 +30,21 @@ func TestDetectSendFileMIMEAddsOpusCodecForOgg(t *testing.T) {
}
}
func TestReadSendFileDataRejectsOversizedFile(t *testing.T) {
path := t.TempDir() + "/huge.bin"
if err := os.WriteFile(path, nil, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.Truncate(path, maxSendFileSize+1); err != nil {
t.Fatalf("Truncate: %v", err)
}
_, err := readSendFileData(path)
if err == nil || !strings.Contains(err.Error(), "file too large") {
t.Fatalf("expected file too large error, got %v", err)
}
}
func TestSendFileCommandExposesReplyFlags(t *testing.T) {
cmd := newSendFileCmd(&rootFlags{})
for _, name := range []string{"reply-to", "reply-to-sender"} {

View File

@ -180,6 +180,9 @@ WhatsApp Web history is best-effort. If you want to try fetching *older* message
- `wacli send file --to PHONE_OR_JID --file PATH [--caption TEXT] [--mime TYPE] [--reply-to MSG_ID] [--reply-to-sender JID]`
- `wacli send react --to PHONE_OR_JID --id MSG_ID [--reaction TEXT] [--sender JID]`
Send-file uploads and media downloads are capped at 100 MiB to avoid reading
or writing unexpectedly large payloads in one command.
### Contacts (read + local management)
- `wacli contacts search <query>`

View File

@ -3,7 +3,6 @@ package wa
import (
"context"
"fmt"
"math"
"os"
"path/filepath"
"strings"
@ -12,6 +11,8 @@ import (
"go.mau.fi/whatsmeow"
)
const MaxMediaDownloadSize = 100 * 1024 * 1024
func MediaTypeFromString(mediaType string) (whatsmeow.MediaType, error) {
switch strings.ToLower(strings.TrimSpace(mediaType)) {
case "image":
@ -61,9 +62,9 @@ func (c *Client) DownloadMediaToFile(ctx context.Context, directPath string, enc
}
}()
length := -1
if fileLength > 0 && fileLength < math.MaxInt32 {
length = int(fileLength)
length, err := mediaDownloadLength(fileLength)
if err != nil {
return 0, err
}
if err := cli.DownloadMediaWithPathToFile(ctx, directPath, encFileHash, fileHash, mediaKey, length, mt, mmsType, tmpFile); err != nil {
@ -86,3 +87,13 @@ func (c *Client) DownloadMediaToFile(ctx context.Context, directPath string, enc
}
return info.Size(), nil
}
func mediaDownloadLength(fileLength uint64) (int, error) {
if fileLength > MaxMediaDownloadSize {
return 0, fmt.Errorf("media too large (%d bytes); maximum download size is %d bytes", fileLength, MaxMediaDownloadSize)
}
if fileLength > 0 {
return int(fileLength), nil
}
return -1, nil
}

View File

@ -1,6 +1,9 @@
package wa
import "testing"
import (
"strings"
"testing"
)
func TestMediaTypeFromString(t *testing.T) {
for _, tc := range []string{"image", "video", "audio", "document"} {
@ -12,3 +15,19 @@ func TestMediaTypeFromString(t *testing.T) {
t.Fatalf("expected error for unsupported type")
}
}
func TestMediaDownloadLengthRejectsOversizedMedia(t *testing.T) {
_, err := mediaDownloadLength(MaxMediaDownloadSize + 1)
if err == nil || !strings.Contains(err.Error(), "media too large") {
t.Fatalf("expected media too large error, got %v", err)
}
}
func TestMediaDownloadLength(t *testing.T) {
if got, err := mediaDownloadLength(0); err != nil || got != -1 {
t.Fatalf("length(0) = %d, %v; want -1, nil", got, err)
}
if got, err := mediaDownloadLength(123); err != nil || got != 123 {
t.Fatalf("length(123) = %d, %v; want 123, nil", got, err)
}
}