PassAGE/security.go
fraggle 3552db50c2 Initial commit: PassAGE password manager
- AGE encryption with master password model
- Core commands: init, show, insert, edit, generate, rm, mv, cp, find, grep, ls
- Git integration for version control
- Clipboard support (X11 and Wayland)
- Secure password generation
- Backup and restore functionality
- Comprehensive security features
- Complete documentation
2026-01-11 18:48:01 -04:00

164 lines
4.3 KiB
Go

package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// checkSneakyPaths prevents path traversal attacks
// Because users will try to break things, and we'd rather not let them
func checkSneakyPaths(path string) error {
if path == "" {
return nil // Empty is fine
}
// Classic path traversal attempt - catch it early
if strings.Contains(path, "..") {
return fmt.Errorf("error: path contains '..' - path traversal not allowed")
}
// No absolute paths either - keep everything relative
if filepath.IsAbs(path) {
return fmt.Errorf("error: absolute paths not allowed")
}
// Check if path would escape store directory
storeDir := getStoreDir()
absPath, err := filepath.Abs(filepath.Join(storeDir, path))
if err != nil {
return fmt.Errorf("error: invalid path: %w", err)
}
absStoreDir, err := filepath.Abs(storeDir)
if err != nil {
return fmt.Errorf("error: invalid store directory: %w", err)
}
// Ensure the resolved path is within the store directory
relPath, err := filepath.Rel(absStoreDir, absPath)
if err != nil {
return fmt.Errorf("error: path outside store directory")
}
if strings.HasPrefix(relPath, "..") {
return fmt.Errorf("error: path escapes store directory")
}
return nil
}
// sanitizePath cleans and validates a password path
func sanitizePath(path string) (string, error) {
if err := checkSneakyPaths(path); err != nil {
return "", err
}
// Clean up the path - normalize separators and such
cleaned := filepath.Clean(path)
// Trim slashes - they're not needed
cleaned = strings.Trim(cleaned, "/")
// Reasonable length limit - 4KB should be enough for anyone (famous last words)
const maxPathLength = 4096
if len(cleaned) > maxPathLength {
return "", fmt.Errorf("error: path too long (max %d characters)", maxPathLength)
}
return cleaned, nil
}
// maxFileSize limits the size of files we'll read into memory
// 10MB seems reasonable - if you need bigger passwords, maybe rethink your life choices
const maxFileSize = 10 * 1024 * 1024
// checkFileSize checks if a file is within size limits
func checkFileSize(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Size() > maxFileSize {
return fmt.Errorf("error: file too large (max %d bytes)", maxFileSize)
}
return nil
}
// maxPasswordLength limits password length to prevent DoS
// 1MB should be enough for even the most paranoid password
const maxPasswordLength = 1024 * 1024
// checkPasswordLength validates password length
func checkPasswordLength(password string) error {
if len(password) > maxPasswordLength {
return fmt.Errorf("error: password too long (max %d bytes)", maxPasswordLength)
}
return nil
}
// readLimited reads up to maxBytes from a reader to prevent resource exhaustion
func readLimited(r io.Reader, maxBytes int64) ([]byte, error) {
limitedReader := io.LimitReader(r, maxBytes)
data, err := io.ReadAll(limitedReader)
if err != nil {
return nil, err
}
// Check if we hit the limit (would mean file is larger than max)
if int64(len(data)) == maxBytes {
// Try to read one more byte to see if there's more
var buf [1]byte
if n, _ := r.Read(buf[:]); n > 0 {
return nil, fmt.Errorf("error: file too large (max %d bytes)", maxBytes)
}
}
return data, nil
}
// createSecureTempFile creates a temporary file in a secure location
// Tries /dev/shm first (RAM-backed, faster, more secure), falls back to system temp
func createSecureTempFile(prefix, suffix string) (string, error) {
// Try /dev/shm - it's RAM-backed so it's faster and doesn't touch disk
// Not available on all systems, but worth checking
if dir := "/dev/shm"; isWritable(dir) {
tmpFile, err := os.CreateTemp(dir, prefix)
if err == nil {
tmpFile.Close()
return tmpFile.Name(), nil
}
// If it fails, just continue to fallback
}
// Fall back to whatever the OS thinks is a good temp directory
tmpFile, err := os.CreateTemp("", prefix)
if err != nil {
return "", err
}
tmpFile.Close()
return tmpFile.Name(), nil
}
// isWritable checks if a directory is writable
func isWritable(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
if !info.IsDir() {
return false
}
// Try to create a test file
testFile := filepath.Join(path, ".passage-write-test")
f, err := os.Create(testFile)
if err != nil {
return false
}
f.Close()
os.Remove(testFile)
return true
}