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 }