ruborg 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3dfe277e22cf29ef1132f479b8e8a969e87c69b383529b7f05eb7238ac5ea07
4
- data.tar.gz: fcf98cabcd5f4636fc513e69e3b446747c9ca2f054f6480f4da60416ec865f0d
3
+ metadata.gz: 0bb6c1c5f47b1fbb4538310a929914b0e744bcdb30d5e5635c4246df778e2113
4
+ data.tar.gz: bda5cf063fe587a047dd36fa9c5e948d8130e47501a53eabef9c34a3af7ae361
5
5
  SHA512:
6
- metadata.gz: 159296240a1cec2e7791edb3689bf032569d8c1f4fda0ac5c1522e92076fec8c719dba3117326c9fcb9e074ffa295ed71a75b500657625d1ce71fab391b441c2
7
- data.tar.gz: f6385fa1ff56520719b1f8b3985931b4083b3139889b34ed4d1dcb5d449eec8ba98e96f030988cf03cdcb5744f47255ef3b7a605e7bb040d6b1f2c0446ab3b81
6
+ metadata.gz: 8828dacb76519557d51876327133491318bd199e589fb695d08a94af1a73a3c34772f022b6630e199a2c1ab9ab7d78dcce043a402bfa0f418ae51262aad80e8f
7
+ data.tar.gz: 99b7e79eb2e50240862f7ade79291d9a76469385f2b7e8949df8e6ac3251f9adcbe2f605e42c09364616c028f10575593c3e0a1df1d020bcf8fc7291a9cdb558
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2025-10-05
11
+
12
+ ### Added
13
+ - `borg_options` configuration for controlling Borg environment variables
14
+ - Repository path validation to prevent creation in system directories
15
+ - Backup path validation and normalization
16
+ - Archive name sanitization (alphanumeric, dash, underscore, dot only)
17
+
18
+ ### Changed
19
+ - Borg environment variables now configurable via `borg_options` (backward compatible)
20
+ - All backup paths are now normalized to absolute paths
21
+ - Custom archive names are automatically sanitized
22
+
23
+ ### Security
24
+ - Fixed command injection vulnerability in Passbolt CLI execution (now uses Open3.capture3)
25
+ - Added path traversal protection for extract operations
26
+ - Implemented symlink resolution and system path protection for --remove-source
27
+ - Changed to YAML.safe_load_file to prevent arbitrary code execution
28
+ - Added log path validation to prevent writing to system directories
29
+ - Added repository path validation (prevents /bin, /etc, /usr, etc.)
30
+ - Added backup path validation (rejects empty/nil paths)
31
+ - Added archive name sanitization (prevents injection attacks)
32
+ - Made Borg environment options configurable for enhanced security
33
+ - Added SECURITY.md with comprehensive security guidelines and best practices
34
+ - Enhanced test coverage for all security features
35
+
10
36
  ## [0.3.0] - 2025-10-05
11
37
 
12
38
  ### Added
data/README.md CHANGED
@@ -18,6 +18,7 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
18
18
  - 🗄️ **Multi-Repository** - Manage multiple backup repositories with different sources
19
19
  - 🔄 **Auto-initialization** - Automatically initialize repositories on first use
20
20
  - ✅ **Well-tested** - Comprehensive test suite with RSpec
21
+ - 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
21
22
 
22
23
  ## Prerequisites
23
24
 
@@ -98,6 +99,11 @@ auto_init: true
98
99
 
99
100
  # Log file path (optional, default: ~/.ruborg/logs/ruborg.log)
100
101
  log_file: /var/log/ruborg.log
102
+
103
+ # Borg environment options (optional)
104
+ borg_options:
105
+ allow_relocated_repo: true # Allow relocated repositories (default: true)
106
+ allow_unencrypted_repo: true # Allow unencrypted repositories (default: true)
101
107
  ```
102
108
 
103
109
  ### Multi-Repository Configuration
@@ -111,6 +117,9 @@ encryption: repokey
111
117
  auto_init: true
112
118
  passbolt:
113
119
  resource_id: "global-passbolt-id"
120
+ borg_options:
121
+ allow_relocated_repo: false
122
+ allow_unencrypted_repo: false
114
123
 
115
124
  # Multiple repositories
116
125
  repositories:
@@ -282,6 +291,33 @@ backup_paths:
282
291
 
283
292
  When enabled, ruborg will automatically run `borg init` if the repository doesn't exist when you run `backup`, `list`, or `info` commands. The passphrase will be retrieved from Passbolt if configured.
284
293
 
294
+ ## Security Configuration
295
+
296
+ Ruborg provides configurable security options via `borg_options`:
297
+
298
+ ```yaml
299
+ borg_options:
300
+ # Control whether to allow access to relocated repositories
301
+ # Set to false in production for enhanced security
302
+ allow_relocated_repo: true # default: true
303
+
304
+ # Control whether to allow access to unencrypted repositories
305
+ # Set to false to enforce encryption
306
+ allow_unencrypted_repo: true # default: true
307
+ ```
308
+
309
+ **Security Features:**
310
+ - **Repository Path Validation**: Prevents creation in system directories (`/bin`, `/etc`, `/usr`, etc.)
311
+ - **Backup Path Validation**: Validates and normalizes all backup source paths
312
+ - **Archive Name Sanitization**: Automatically sanitizes custom archive names
313
+ - **Path Traversal Protection**: Prevents extraction to system directories
314
+ - **Symlink Protection**: Resolves and validates symlinks before deletion with `--remove-source`
315
+ - **Safe YAML Loading**: Uses `YAML.safe_load_file` to prevent code execution
316
+ - **Command Injection Protection**: Uses safe command execution methods
317
+ - **Log Path Validation**: Prevents writing logs to system directories
318
+
319
+ See [SECURITY.md](SECURITY.md) for detailed security information and best practices.
320
+
285
321
  ## Command Reference
286
322
 
287
323
  | Command | Description | Options |
@@ -341,6 +377,7 @@ The test suite includes:
341
377
  - Passbolt integration (mocked)
342
378
  - CLI commands
343
379
  - Logging functionality
380
+ - Comprehensive security tests (path validation, sanitization, etc.)
344
381
 
345
382
  ## Contributing
346
383
 
data/SECURITY.md ADDED
@@ -0,0 +1,173 @@
1
+ # Security Policy
2
+
3
+ ## Security Features
4
+
5
+ Ruborg implements several security measures to protect your backup operations:
6
+
7
+ ### 1. Command Injection Prevention
8
+ - Uses `Open3.capture3` for Passbolt CLI execution (no shell interpolation)
9
+ - Array-based command construction for Borg commands
10
+ - No user input directly interpolated into shell commands
11
+
12
+ ### 2. Path Traversal Protection
13
+ - Validates all destination paths during restore operations
14
+ - Prevents extraction to system directories (`/`, `/etc`, `/bin`, `/usr`, etc.)
15
+ - Normalizes paths to prevent `../` traversal attacks
16
+
17
+ ### 3. Symlink Protection
18
+ - Resolves symlinks before file deletion with `--remove-source`
19
+ - Refuses to delete system directories even when targeted via symlinks
20
+ - Uses `FileUtils.rm_rf` with `secure: true` option
21
+
22
+ ### 4. Safe YAML Loading
23
+ - Uses `YAML.safe_load_file` to prevent arbitrary code execution
24
+ - Rejects YAML files containing Ruby objects or other dangerous constructs
25
+ - Only permits basic data types and Symbol class
26
+
27
+ ### 5. Log Path Validation
28
+ - Validates log file paths to prevent writing to system directories
29
+ - Automatically creates log directories with proper permissions
30
+ - Rejects paths in `/bin`, `/etc`, `/usr`, and other sensitive locations
31
+
32
+ ### 6. Passphrase Handling
33
+ - Passes passphrases via environment variables (never CLI arguments)
34
+ - Prevents passphrase leakage in process listings
35
+ - Uses Passbolt integration for secure password retrieval
36
+
37
+ ### 7. Repository Path Validation
38
+ - Validates repository paths to prevent creation in system directories
39
+ - Rejects empty or nil repository paths
40
+ - Prevents accidental repository creation in `/bin`, `/etc`, `/usr`, etc.
41
+
42
+ ### 8. Backup Path Validation
43
+ - Validates all backup source paths before creating archives
44
+ - Rejects empty, nil, or whitespace-only paths
45
+ - Normalizes relative paths to absolute paths for consistency
46
+
47
+ ### 9. Archive Name Sanitization
48
+ - Sanitizes user-provided archive names to prevent injection attacks
49
+ - Allows only alphanumeric characters, dashes, underscores, and dots
50
+ - Rejects archive names that would become empty after sanitization
51
+
52
+ ### 10. Configurable Borg Environment Options
53
+ - Allows control over relocated repository access via config
54
+ - Allows control over unencrypted repository access via config
55
+ - Defaults to safe settings while maintaining backward compatibility
56
+
57
+ ## Security Best Practices
58
+
59
+ ### When Using `--remove-source`
60
+ ⚠️ **Warning**: The `--remove-source` flag permanently deletes source files after backup.
61
+
62
+ **Recommendations:**
63
+ 1. **Test first** without `--remove-source` to verify backups work
64
+ 2. **Never use on symlinks** to critical system directories
65
+ 3. **Verify backups** before using this flag in production
66
+ 4. **Use absolute paths** in configuration to avoid ambiguity
67
+
68
+ ### Configuration File Security
69
+ - Store configuration files with restricted permissions: `chmod 600 ruborg.yml`
70
+ - Never commit Passbolt resource IDs to public repositories
71
+ - Use environment variables for sensitive paths when possible
72
+
73
+ ### Repository Security
74
+ - Use encrypted repositories (default: `encryption: repokey`)
75
+ - Store passphrases in Passbolt, not in config files
76
+ - Use different encryption keys for different repository types
77
+ - Regularly rotate Passbolt passphrases
78
+ - Avoid creating repositories in system directories
79
+ - Use absolute paths for repository locations
80
+
81
+ ### Multi-Repository Considerations
82
+ - Each repository can have its own Passbolt resource ID
83
+ - Validate all source paths before adding to configuration
84
+ - Review exclude patterns to ensure no sensitive files leak
85
+
86
+ ### Archive Naming
87
+ - Use default timestamp-based names when possible
88
+ - If providing custom names, use only alphanumeric characters, dashes, and underscores
89
+ - Avoid special characters or path separators in archive names
90
+
91
+ ### Borg Environment Options
92
+ - Consider disabling `allow_relocated_repo` for production environments
93
+ - Consider disabling `allow_unencrypted_repo` for sensitive data
94
+ - Configure in `ruborg.yml`:
95
+ ```yaml
96
+ borg_options:
97
+ allow_relocated_repo: false # Reject relocated repositories
98
+ allow_unencrypted_repo: false # Reject unencrypted repositories
99
+ ```
100
+
101
+ ## Reporting Security Issues
102
+
103
+ If you discover a security vulnerability, please:
104
+
105
+ 1. **Do NOT** open a public issue
106
+ 2. Email security concerns to: mpantel@aegean.gr
107
+ 3. Include:
108
+ - Description of the vulnerability
109
+ - Steps to reproduce
110
+ - Potential impact
111
+ - Suggested fix (if any)
112
+
113
+ We will respond within 48 hours and work with you to address the issue.
114
+
115
+ ## Security Audit History
116
+
117
+ - **v0.3.1** (2025-10-05): Comprehensive security hardening
118
+ - Fixed command injection in Passbolt CLI execution (uses Open3.capture3)
119
+ - Added path traversal protection for extract operations
120
+ - Implemented symlink protection for file deletion with --remove-source
121
+ - Switched to safe YAML loading (YAML.safe_load_file)
122
+ - Added log path validation to prevent writing to system directories
123
+ - Added repository path validation to prevent creation in system directories
124
+ - Added backup path validation and normalization
125
+ - Implemented archive name sanitization
126
+ - Made Borg environment variables configurable
127
+ - Enhanced test coverage for all security features
128
+ - Created comprehensive SECURITY.md documentation
129
+
130
+ - **v0.3.0** (2025-10-05): Multi-repository and auto-initialization features
131
+ - Added multi-repository configuration support
132
+ - Added auto-initialization feature
133
+ - Added configurable log file paths
134
+ - No security-specific changes in this version
135
+
136
+ ## Dependency Security
137
+
138
+ Ruborg relies on:
139
+ - **Borg Backup**: Industry-standard backup tool with strong encryption
140
+ - **Passbolt CLI**: Secure password management
141
+ - **Ruby stdlib**: No external gems for core functionality (only Thor for CLI)
142
+
143
+ Keep dependencies updated:
144
+ ```bash
145
+ # Update Borg
146
+ brew upgrade borgbackup # macOS
147
+ sudo apt update && sudo apt upgrade borgbackup # Ubuntu
148
+
149
+ # Update Passbolt CLI
150
+ # Follow https://github.com/passbolt/go-passbolt-cli
151
+
152
+ # Update Ruby gems
153
+ bundle update
154
+ ```
155
+
156
+ ## Security Checklist
157
+
158
+ Before deploying ruborg in production:
159
+
160
+ - [ ] Review all paths in configuration files
161
+ - [ ] Set proper file permissions on config (600) and logs (640)
162
+ - [ ] Use Passbolt for all passphrases (never hardcode)
163
+ - [ ] Test restore operations before relying on backups
164
+ - [ ] Never use `--remove-source` without thorough testing
165
+ - [ ] Keep Borg and Passbolt CLI up to date
166
+ - [ ] Review exclude patterns for sensitive data
167
+ - [ ] Use absolute paths in configuration
168
+ - [ ] Enable auto_init only for trusted repository locations
169
+ - [ ] Regularly audit backup logs for anomalies
170
+ - [ ] Validate repository paths are not in system directories
171
+ - [ ] Configure borg_options for your security requirements
172
+ - [ ] Use default archive names or sanitized custom names only
173
+ - [ ] Ensure backup paths don't contain empty or nil values
data/lib/ruborg/backup.rb CHANGED
@@ -28,8 +28,12 @@ module Ruborg
28
28
  # Change to destination directory if specified
29
29
  if destination != "."
30
30
  require "fileutils"
31
- FileUtils.mkdir_p(destination) unless File.directory?(destination)
32
- Dir.chdir(destination) do
31
+
32
+ # Validate and normalize destination path
33
+ validated_dest = validate_destination_path(destination)
34
+ FileUtils.mkdir_p(validated_dest) unless File.directory?(validated_dest)
35
+
36
+ Dir.chdir(validated_dest) do
33
37
  execute_borg_command(cmd)
34
38
  end
35
39
  else
@@ -57,7 +61,10 @@ module Ruborg
57
61
  end
58
62
 
59
63
  cmd << "#{@repository.path}::#{archive_name}"
60
- cmd += @config.backup_paths
64
+
65
+ # Validate and normalize backup paths
66
+ validated_paths = validate_backup_paths(@config.backup_paths)
67
+ cmd += validated_paths
61
68
 
62
69
  cmd
63
70
  end
@@ -79,11 +86,53 @@ module Ruborg
79
86
  require "fileutils"
80
87
 
81
88
  @config.backup_paths.each do |path|
82
- if File.directory?(path)
83
- FileUtils.rm_rf(path)
84
- elsif File.file?(path)
85
- FileUtils.rm(path)
89
+ # Resolve symlinks and validate path
90
+ begin
91
+ real_path = File.realpath(path)
92
+ rescue Errno::ENOENT
93
+ # Path doesn't exist, skip
94
+ next
95
+ end
96
+
97
+ # Security check: ensure path hasn't been tampered with
98
+ unless File.exist?(real_path)
99
+ next
86
100
  end
101
+
102
+ # Additional safety: don't delete root or system directories
103
+ if real_path == "/" || real_path.start_with?("/bin", "/sbin", "/usr", "/etc", "/sys", "/proc")
104
+ raise BorgError, "Refusing to delete system path: #{real_path}"
105
+ end
106
+
107
+ if File.directory?(real_path)
108
+ FileUtils.rm_rf(real_path, secure: true)
109
+ elsif File.file?(real_path)
110
+ FileUtils.rm(real_path)
111
+ end
112
+ end
113
+ end
114
+
115
+ def validate_destination_path(destination)
116
+ # Expand and normalize the path
117
+ normalized_path = File.expand_path(destination)
118
+
119
+ # Security check: prevent path traversal to sensitive directories
120
+ forbidden_paths = ["/", "/bin", "/sbin", "/usr", "/etc", "/sys", "/proc", "/boot"]
121
+ forbidden_paths.each do |forbidden|
122
+ if normalized_path == forbidden || normalized_path.start_with?("#{forbidden}/")
123
+ raise BorgError, "Invalid destination: refusing to extract to system directory #{normalized_path}"
124
+ end
125
+ end
126
+
127
+ normalized_path
128
+ end
129
+
130
+ def validate_backup_paths(paths)
131
+ raise BorgError, "No backup paths specified" if paths.nil? || paths.empty?
132
+
133
+ paths.map do |path|
134
+ raise BorgError, "Empty backup path specified" if path.nil? || path.to_s.strip.empty?
135
+ File.expand_path(path)
87
136
  end
88
137
  end
89
138
  end
data/lib/ruborg/cli.rb CHANGED
@@ -17,10 +17,14 @@ module Ruborg
17
17
  # Try to load config to get log_file setting
18
18
  config_path = options[:config] || "ruborg.yml"
19
19
  if File.exist?(config_path)
20
- config_data = YAML.load_file(config_path) rescue {}
20
+ config_data = YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) rescue {}
21
21
  log_path = config_data["log_file"]
22
22
  end
23
23
  end
24
+
25
+ # Validate log path if provided
26
+ log_path = validate_log_path(log_path) if log_path
27
+
24
28
  @logger = RuborgLogger.new(log_file: log_path)
25
29
  end
26
30
 
@@ -63,7 +67,7 @@ module Ruborg
63
67
  config = Config.new(options[:config])
64
68
  passphrase = fetch_passphrase_from_config(config)
65
69
 
66
- repo = Repository.new(config.repository, passphrase: passphrase)
70
+ repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
67
71
 
68
72
  # Auto-initialize repository if configured
69
73
  if config.auto_init? && !repo.exists?
@@ -88,7 +92,7 @@ module Ruborg
88
92
  config = Config.new(options[:config])
89
93
  passphrase = fetch_passphrase_from_config(config)
90
94
 
91
- repo = Repository.new(config.repository, passphrase: passphrase)
95
+ repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
92
96
  backup = Backup.new(repo, config: config)
93
97
 
94
98
  backup.extract(archive_name, destination: options[:destination], path: options[:path])
@@ -110,7 +114,7 @@ module Ruborg
110
114
  config = Config.new(options[:config])
111
115
  passphrase = fetch_passphrase_from_config(config)
112
116
 
113
- repo = Repository.new(config.repository, passphrase: passphrase)
117
+ repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
114
118
 
115
119
  # Auto-initialize repository if configured
116
120
  if config.auto_init? && !repo.exists?
@@ -147,12 +151,37 @@ module Ruborg
147
151
  exit 1
148
152
  end
149
153
 
154
+ def validate_log_path(log_path)
155
+ # Expand to absolute path
156
+ normalized_path = File.expand_path(log_path)
157
+
158
+ # Prevent writing to sensitive system directories
159
+ forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
160
+ forbidden_paths.each do |forbidden|
161
+ if normalized_path.start_with?("#{forbidden}/")
162
+ raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
163
+ end
164
+ end
165
+
166
+ # Ensure parent directory exists or can be created
167
+ log_dir = File.dirname(normalized_path)
168
+ unless File.directory?(log_dir)
169
+ begin
170
+ FileUtils.mkdir_p(log_dir)
171
+ rescue => e
172
+ raise ConfigError, "Cannot create log directory #{log_dir}: #{e.message}"
173
+ end
174
+ end
175
+
176
+ normalized_path
177
+ end
178
+
150
179
  # Single repository backup (legacy)
151
180
  def backup_single_repo(config)
152
181
  @logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
153
182
  passphrase = fetch_passphrase_from_config(config)
154
183
 
155
- repo = Repository.new(config.repository, passphrase: passphrase)
184
+ repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
156
185
 
157
186
  # Auto-initialize repository if configured
158
187
  if config.auto_init? && !repo.exists?
@@ -163,9 +192,9 @@ module Ruborg
163
192
 
164
193
  backup = Backup.new(repo, config: config)
165
194
 
166
- archive_name = options[:name] || Time.now.strftime("%Y-%m-%d_%H-%M-%S")
195
+ archive_name = options[:name] ? sanitize_archive_name(options[:name]) : Time.now.strftime("%Y-%m-%d_%H-%M-%S")
167
196
  @logger.info("Creating archive: #{archive_name}")
168
- backup.create(name: options[:name], remove_source: options[:remove_source])
197
+ backup.create(name: archive_name, remove_source: options[:remove_source])
169
198
  @logger.info("Backup created successfully: #{archive_name}")
170
199
 
171
200
  if options[:remove_source]
@@ -203,7 +232,8 @@ module Ruborg
203
232
  merged_config = global_settings.merge(repo_config)
204
233
 
205
234
  passphrase = fetch_passphrase_for_repo(merged_config)
206
- repo = Repository.new(repo_config["path"], passphrase: passphrase)
235
+ borg_opts = merged_config["borg_options"] || {}
236
+ repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts)
207
237
 
208
238
  # Auto-initialize if configured
209
239
  auto_init = merged_config["auto_init"] || false
@@ -217,7 +247,7 @@ module Ruborg
217
247
  backup_config = BackupConfig.new(repo_config, merged_config)
218
248
  backup = Backup.new(repo, config: backup_config)
219
249
 
220
- archive_name = options[:name] || "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
250
+ archive_name = options[:name] ? sanitize_archive_name(options[:name]) : "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
221
251
  @logger.info("Creating archive: #{archive_name}")
222
252
 
223
253
  sources = repo_config["sources"] || []
@@ -237,6 +267,20 @@ module Ruborg
237
267
  Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
238
268
  end
239
269
 
270
+ def sanitize_archive_name(name)
271
+ raise ConfigError, "Archive name cannot be empty" if name.nil? || name.strip.empty?
272
+
273
+ # Check if name contains at least one valid character before sanitization
274
+ unless name =~ /[a-zA-Z0-9._-]/
275
+ raise ConfigError, "Invalid archive name: must contain at least one valid character (alphanumeric, dot, dash, or underscore)"
276
+ end
277
+
278
+ # Allow only alphanumeric, dash, underscore, and dot
279
+ sanitized = name.gsub(/[^a-zA-Z0-9._-]/, '_')
280
+
281
+ sanitized
282
+ end
283
+
240
284
  # Wrapper class to adapt multi-repo config to existing Backup class
241
285
  class BackupConfig
242
286
  def initialize(repo_config, merged_settings)
data/lib/ruborg/config.rb CHANGED
@@ -17,9 +17,11 @@ module Ruborg
17
17
  def load_config
18
18
  raise ConfigError, "Configuration file not found: #{@config_path}" unless File.exist?(@config_path)
19
19
 
20
- @data = YAML.load_file(@config_path)
20
+ @data = YAML.safe_load_file(@config_path, permitted_classes: [Symbol], aliases: true)
21
21
  rescue Psych::SyntaxError => e
22
22
  raise ConfigError, "Invalid YAML syntax: #{e.message}"
23
+ rescue Psych::DisallowedClass => e
24
+ raise ConfigError, "Invalid YAML content: #{e.message}"
23
25
  end
24
26
 
25
27
  # Legacy single-repo accessors (for backward compatibility)
@@ -32,15 +34,18 @@ module Ruborg
32
34
  end
33
35
 
34
36
  def exclude_patterns
35
- @data["exclude_patterns"] || []
37
+ patterns = @data["exclude_patterns"] || []
38
+ validate_exclude_patterns(patterns)
36
39
  end
37
40
 
38
41
  def compression
39
- @data["compression"] || "lz4"
42
+ value = @data["compression"] || "lz4"
43
+ validate_compression(value)
40
44
  end
41
45
 
42
46
  def encryption_mode
43
- @data["encryption"] || "repokey"
47
+ value = @data["encryption"] || "repokey"
48
+ validate_encryption(value)
44
49
  end
45
50
 
46
51
  def passbolt_integration
@@ -55,6 +60,10 @@ module Ruborg
55
60
  @data["log_file"]
56
61
  end
57
62
 
63
+ def borg_options
64
+ @data["borg_options"] || {}
65
+ end
66
+
58
67
  # New multi-repo support
59
68
  def multi_repo?
60
69
  @multi_repo
@@ -81,8 +90,42 @@ module Ruborg
81
90
 
82
91
  private
83
92
 
93
+ VALID_COMPRESSION = ["lz4", "zstd", "zlib", "lzma", "none"].freeze
94
+ VALID_ENCRYPTION = ["repokey", "keyfile", "none", "authenticated", "repokey-blake2",
95
+ "keyfile-blake2", "authenticated-blake2"].freeze
96
+
84
97
  def detect_format
85
98
  @multi_repo = @data.key?("repositories")
86
99
  end
100
+
101
+ def validate_compression(compression)
102
+ unless VALID_COMPRESSION.include?(compression)
103
+ raise ConfigError, "Invalid compression '#{compression}'. Must be one of: #{VALID_COMPRESSION.join(', ')}"
104
+ end
105
+ compression
106
+ end
107
+
108
+ def validate_encryption(encryption)
109
+ unless VALID_ENCRYPTION.include?(encryption)
110
+ raise ConfigError, "Invalid encryption mode '#{encryption}'. Must be one of: #{VALID_ENCRYPTION.join(', ')}"
111
+ end
112
+ encryption
113
+ end
114
+
115
+ def validate_exclude_patterns(patterns)
116
+ return patterns if patterns.empty?
117
+
118
+ patterns.each do |pattern|
119
+ if pattern.nil? || pattern.to_s.strip.empty?
120
+ raise ConfigError, "Exclude pattern cannot be empty or nil"
121
+ end
122
+
123
+ if pattern.length > 1000
124
+ raise ConfigError, "Exclude pattern too long (max 1000 characters): #{pattern[0..50]}..."
125
+ end
126
+ end
127
+
128
+ patterns
129
+ end
87
130
  end
88
131
  end
data/lib/ruborg/logger.rb CHANGED
@@ -10,7 +10,7 @@ module Ruborg
10
10
 
11
11
  def initialize(log_file: nil)
12
12
  @log_file = log_file || default_log_file
13
- ensure_log_directory
13
+ validate_and_ensure_log_directory
14
14
  @logger = Logger.new(@log_file, "daily")
15
15
  @logger.level = Logger::INFO
16
16
  @logger.formatter = proc do |severity, datetime, progname, msg|
@@ -45,8 +45,21 @@ module Ruborg
45
45
  dir
46
46
  end
47
47
 
48
- def ensure_log_directory
49
- FileUtils.mkdir_p(File.dirname(@log_file)) unless File.directory?(File.dirname(@log_file))
48
+ def validate_and_ensure_log_directory
49
+ # Validate log file path for security
50
+ normalized_path = File.expand_path(@log_file)
51
+
52
+ # Prevent writing to sensitive system directories
53
+ forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
54
+ forbidden_paths.each do |forbidden|
55
+ if normalized_path.start_with?("#{forbidden}/")
56
+ raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
57
+ end
58
+ end
59
+
60
+ # Ensure log directory exists
61
+ log_dir = File.dirname(normalized_path)
62
+ FileUtils.mkdir_p(log_dir) unless File.directory?(log_dir)
50
63
  end
51
64
  end
52
65
  end
@@ -30,8 +30,9 @@ module Ruborg
30
30
  end
31
31
 
32
32
  def execute_command(cmd)
33
- output = `#{cmd.join(' ')}`
34
- [output, $?.success?]
33
+ require "open3"
34
+ stdout, stderr, status = Open3.capture3(*cmd)
35
+ [stdout, status.success?]
35
36
  end
36
37
 
37
38
  def parse_password(json_output)
@@ -5,9 +5,10 @@ module Ruborg
5
5
  class Repository
6
6
  attr_reader :path
7
7
 
8
- def initialize(path, passphrase: nil)
9
- @path = path
8
+ def initialize(path, passphrase: nil, borg_options: {})
9
+ @path = validate_repo_path(path)
10
10
  @passphrase = passphrase
11
+ @borg_options = borg_options
11
12
  end
12
13
 
13
14
  def exists?
@@ -37,11 +38,32 @@ module Ruborg
37
38
 
38
39
  private
39
40
 
41
+ def validate_repo_path(path)
42
+ raise BorgError, "Repository path cannot be empty" if path.nil? || path.empty?
43
+
44
+ normalized = File.expand_path(path)
45
+
46
+ # Prevent repository creation in critical system directories
47
+ forbidden = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot", "/dev"]
48
+ forbidden.each do |forbidden_path|
49
+ if normalized == forbidden_path || normalized.start_with?("#{forbidden_path}/")
50
+ raise BorgError, "Invalid repository path: refusing to use system directory #{normalized}"
51
+ end
52
+ end
53
+
54
+ normalized
55
+ end
56
+
40
57
  def execute_borg_command(cmd)
41
58
  env = {}
42
59
  env["BORG_PASSPHRASE"] = @passphrase if @passphrase
43
- env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = "yes"
44
- env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = "yes"
60
+
61
+ # Apply Borg environment options from config (defaults to yes for backward compatibility)
62
+ allow_relocated = @borg_options.fetch("allow_relocated_repo", true)
63
+ allow_unencrypted = @borg_options.fetch("allow_unencrypted_repo", true)
64
+
65
+ env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = allow_relocated ? "yes" : "no"
66
+ env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = allow_unencrypted ? "yes" : "no"
45
67
 
46
68
  # Redirect stdin from /dev/null to prevent interactive prompts
47
69
  result = system(env, *cmd, in: "/dev/null")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruborg
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruborg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michail Pantelelis
@@ -110,6 +110,7 @@ files:
110
110
  - LICENSE
111
111
  - README.md
112
112
  - Rakefile
113
+ - SECURITY.md
113
114
  - exe/ruborg
114
115
  - lib/ruborg.rb
115
116
  - lib/ruborg/backup.rb