Resolve merge conflict: combine PR 1536 media tempdir with PR 1535 size limit
This commit is contained in:
commit
3d300c57bb
7 changed files with 60 additions and 16 deletions
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue