security: cap media transfer sizes
This commit is contained in:
parent
515cd43b9f
commit
83d89da341
@ -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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"} {
|
||||
|
||||
@ -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>`
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user