Resolve merge conflict: combine PR 1536 media tempdir with PR 1535 size limit

This commit is contained in:
OpenClaw-User 2026-03-14 16:51:25 +08:00
commit 3d300c57bb
7 changed files with 60 additions and 16 deletions

View file

@ -636,7 +636,9 @@ func (c *FeishuChannel) downloadResource(
return "" return ""
} }
if _, copyErr := io.Copy(out, resp.File); copyErr != nil { maxSize := int64(utils.MaxMediaDownloadSize)
written, copyErr := io.CopyN(out, resp.File, maxSize)
if copyErr != nil && copyErr != io.EOF {
out.Close() out.Close()
os.Remove(localPath) os.Remove(localPath)
logger.ErrorCF("feishu", "Failed to write resource to file", map[string]any{ logger.ErrorCF("feishu", "Failed to write resource to file", map[string]any{
@ -644,6 +646,14 @@ func (c *FeishuChannel) downloadResource(
}) })
return "" return ""
} }
if written >= maxSize {
out.Close()
os.Remove(localPath)
logger.ErrorCF("feishu", "Resource exceeds size limit, download aborted", map[string]any{
"limit_mb": maxSize / (1 << 20),
})
return ""
}
out.Close() out.Close()
ref, err := store.Store(localPath, media.MediaMeta{ ref, err := store.Store(localPath, media.MediaMeta{

View file

@ -663,7 +663,7 @@ func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload any)
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil { if err != nil {
return channels.ClassifySendError(resp.StatusCode, fmt.Errorf("reading LINE API error response: %w", err)) return channels.ClassifySendError(resp.StatusCode, fmt.Errorf("reading LINE API error response: %w", err))
} }

View file

@ -725,13 +725,15 @@ func (c *MatrixChannel) downloadMedia(
reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second) reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second)
defer cancel() defer cancel()
const maxMediaSize = 50 << 20 // 50 MB (PR #1535: prevent disk exhaustion)
resp, err := c.client.Download(reqCtx, parsed) resp, err := c.client.Download(reqCtx, parsed)
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
reader := resp.Body limitedBody := io.LimitReader(resp.Body, maxMediaSize)
reader := limitedBody
readerClose := func() error { return nil } readerClose := func() error { return nil }
// Encrypted attachments put URL in msgEvt.File and require client-side decryption. // Encrypted attachments put URL in msgEvt.File and require client-side decryption.
@ -739,7 +741,7 @@ func (c *MatrixChannel) downloadMedia(
if err = msgEvt.File.PrepareForDecryption(); err != nil { if err = msgEvt.File.PrepareForDecryption(); err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err) return "", fmt.Errorf("decrypt matrix media: %w", err)
} }
decryptReader := msgEvt.File.DecryptStream(resp.Body) decryptReader := msgEvt.File.DecryptStream(limitedBody)
reader = decryptReader reader = decryptReader
readerClose = decryptReader.Close readerClose = decryptReader.Close
} }

View file

@ -793,7 +793,7 @@ func (c *WeComAIBotChannel) sendViaResponseURL(responseURL, content string) erro
return nil return nil
} }
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil { if err != nil {
return fmt.Errorf("reading response_url body: %w: %w", channels.ErrTemporary, err) return fmt.Errorf("reading response_url body: %w: %w", channels.ErrTemporary, err)
} }

View file

@ -320,8 +320,9 @@ func (c *WeComAppChannel) uploadMedia(ctx context.Context, accessToken, mediaTyp
} }
defer resp.Body.Close() defer resp.Body.Close()
const maxRespSize = 1 << 20 // 1 MB
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
respBody, readErr := io.ReadAll(resp.Body) respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, maxRespSize))
if readErr != nil { if readErr != nil {
return "", channels.ClassifySendError( return "", channels.ClassifySendError(
resp.StatusCode, resp.StatusCode,
@ -379,8 +380,9 @@ func (c *WeComAppChannel) sendWeComMessage(ctx context.Context, accessToken stri
} }
defer resp.Body.Close() defer resp.Body.Close()
const maxSendRespSize = 1 << 20 // 1 MB
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
respBody, readErr := io.ReadAll(resp.Body) respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, maxSendRespSize))
if readErr != nil { if readErr != nil {
return channels.ClassifySendError( return channels.ClassifySendError(
resp.StatusCode, resp.StatusCode,
@ -393,7 +395,7 @@ func (c *WeComAppChannel) sendWeComMessage(ctx context.Context, accessToken stri
) )
} }
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxSendRespSize))
if err != nil { if err != nil {
return fmt.Errorf("failed to read response: %w", err) return fmt.Errorf("failed to read response: %w", err)
} }
@ -550,13 +552,18 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
return return
} }
// Read request body // Read request body (limit to 4 MB to prevent memory exhaustion).
body, err := io.ReadAll(r.Body) const maxBodySize = 4 << 20 // 4 MB
body, err := io.ReadAll(io.LimitReader(r.Body, maxBodySize+1))
if err != nil { if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest) http.Error(w, "Failed to read body", http.StatusBadRequest)
return return
} }
defer r.Body.Close() defer r.Body.Close()
if len(body) > maxBodySize {
http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge)
return
}
// Parse XML to get encrypted message // Parse XML to get encrypted message
var encryptedMsg struct { var encryptedMsg struct {
@ -697,7 +704,7 @@ func (c *WeComAppChannel) refreshAccessToken() error {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil { if err != nil {
return fmt.Errorf("failed to read response: %w", err) return fmt.Errorf("failed to read response: %w", err)
} }

View file

@ -253,13 +253,18 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp
return return
} }
// Read request body // Read request body (limit to 4 MB to prevent memory exhaustion).
body, err := io.ReadAll(r.Body) const maxBodySize = 4 << 20 // 4 MB
body, err := io.ReadAll(io.LimitReader(r.Body, maxBodySize+1))
if err != nil { if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest) http.Error(w, "Failed to read body", http.StatusBadRequest)
return return
} }
defer r.Body.Close() defer r.Body.Close()
if len(body) > maxBodySize {
http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge)
return
}
// Parse XML to get encrypted message // Parse XML to get encrypted message
var encryptedMsg struct { var encryptedMsg struct {
@ -452,8 +457,9 @@ func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID, content
} }
defer resp.Body.Close() defer resp.Body.Close()
const maxRespSize = 1 << 20 // 1 MB
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
body, readErr := io.ReadAll(resp.Body) body, readErr := io.ReadAll(io.LimitReader(resp.Body, maxRespSize))
if readErr != nil { if readErr != nil {
return channels.ClassifySendError( return channels.ClassifySendError(
resp.StatusCode, resp.StatusCode,
@ -466,7 +472,7 @@ func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID, content
) )
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(io.LimitReader(resp.Body, maxRespSize))
if err != nil { if err != nil {
return fmt.Errorf("failed to read response: %w", err) return fmt.Errorf("failed to read response: %w", err)
} }

View file

@ -49,12 +49,17 @@ func SanitizeFilename(filename string) string {
return base return base
} }
// MaxMediaDownloadSize is the upper bound for media file downloads (50 MB).
// Prevents disk exhaustion from oversized or malicious attachments.
const MaxMediaDownloadSize = 50 << 20
// DownloadOptions holds optional parameters for downloading files // DownloadOptions holds optional parameters for downloading files
type DownloadOptions struct { type DownloadOptions struct {
Timeout time.Duration Timeout time.Duration
ExtraHeaders map[string]string ExtraHeaders map[string]string
LoggerPrefix string LoggerPrefix string
ProxyURL string ProxyURL string
MaxSize int64 // 0 = use MaxMediaDownloadSize default
} }
// DownloadFile downloads a file from URL to a local temp directory. // DownloadFile downloads a file from URL to a local temp directory.
@ -135,7 +140,12 @@ func DownloadFile(urlStr, filename string, opts DownloadOptions) string {
} }
defer out.Close() defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil { maxSize := opts.MaxSize
if maxSize <= 0 {
maxSize = MaxMediaDownloadSize
}
written, err := io.CopyN(out, resp.Body, maxSize)
if err != nil && err != io.EOF {
out.Close() out.Close()
os.Remove(localPath) os.Remove(localPath)
logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{ logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{
@ -143,6 +153,15 @@ func DownloadFile(urlStr, filename string, opts DownloadOptions) string {
}) })
return "" return ""
} }
// Check if there is more data beyond the limit (oversized file).
if written >= maxSize {
out.Close()
os.Remove(localPath)
logger.ErrorCF(opts.LoggerPrefix, "File exceeds size limit, download aborted", map[string]any{
"limit_mb": maxSize / (1 << 20),
})
return ""
}
logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]any{ logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]any{
"path": localPath, "path": localPath,