Auto-commit 2026-04-27 19:13
This commit is contained in:
@@ -4,19 +4,62 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidAttachmentID is returned when attachmentID contains unsafe characters
|
||||
var ErrInvalidAttachmentID = os.ErrInvalid
|
||||
|
||||
type AttachmentManager struct {
|
||||
attachmentsDir string
|
||||
}
|
||||
|
||||
const maxUploadSize = 50 * 1024 * 1024 // 50MB
|
||||
|
||||
func NewAttachmentManager() *AttachmentManager {
|
||||
return &AttachmentManager{
|
||||
attachmentsDir: filepath.Join(os.Getenv("HOME"), ".config", "pop", "attachments"),
|
||||
}
|
||||
}
|
||||
|
||||
// isAttachmentIDSafe validates that attachmentID contains only safe characters
|
||||
// to prevent path traversal attacks
|
||||
func isAttachmentIDSafe(id string) bool {
|
||||
if id == "" {
|
||||
return false
|
||||
}
|
||||
// Only allow alphanumeric, hyphen, and underscore
|
||||
for _, r := range id {
|
||||
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') || r == '-' || r == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// sanitizeAttachmentID ensures the attachmentID is safe and the resolved path
|
||||
// is within the attachments directory
|
||||
func sanitizeAttachmentID(id string) (string, error) {
|
||||
if !isAttachmentIDSafe(id) {
|
||||
return "", ErrInvalidAttachmentID
|
||||
}
|
||||
// Use filepath.Clean to resolve any .. or . components
|
||||
cleanID := filepath.Clean(id)
|
||||
if cleanID != id {
|
||||
return "", ErrInvalidAttachmentID
|
||||
}
|
||||
return cleanID, nil
|
||||
}
|
||||
|
||||
func (m *AttachmentManager) Download(attachmentID, name, destPath string) error {
|
||||
// Sanitize attachmentID to prevent path traversal
|
||||
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
attachmentID = sanitizedID
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(m.attachmentsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -37,26 +80,45 @@ func (m *AttachmentManager) Download(attachmentID, name, destPath string) error
|
||||
}
|
||||
|
||||
func (m *AttachmentManager) Upload(attachmentID, name string, reader io.Reader) error {
|
||||
// Sanitize attachmentID to prevent path traversal
|
||||
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
attachmentID = sanitizedID
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(m.attachmentsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(m.attachmentsDir, attachmentID)
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
// Limit reader to maxUploadSize to prevent DoS
|
||||
limitedReader := io.LimitReader(reader, maxUploadSize)
|
||||
data, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
return os.WriteFile(filepath.Join(m.attachmentsDir, attachmentID), data, 0644)
|
||||
}
|
||||
|
||||
func (m *AttachmentManager) Get(attachmentID string) ([]byte, error) {
|
||||
// Sanitize attachmentID to prevent path traversal
|
||||
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
attachmentID = sanitizedID
|
||||
}
|
||||
path := filepath.Join(m.attachmentsDir, attachmentID)
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
|
||||
func (m *AttachmentManager) Delete(attachmentID string) error {
|
||||
// Sanitize attachmentID to prevent path traversal
|
||||
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
attachmentID = sanitizedID
|
||||
}
|
||||
path := filepath.Join(m.attachmentsDir, attachmentID)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user