fix(core,cli): harden backup-restore tar unpack against path traversal (audit S2)
cmd_backup_restore previously called tar::Archive::unpack with default settings, allowing malicious .relbak archives to escape the target directory via .. entries, absolute paths, or symlinks. No size cap meant tar bombs could exhaust disk space. Replaced with relicario_core::safe_unpack_git_archive which: - Rejects .. (ParentDir), absolute (RootDir), and drive-prefix (Prefix) components with "path traversal blocked" error. - Rejects symlinks and hardlinks outright. - Checks declared header size before reading body; rejects entries or cumulative totals exceeding the caller's cap. - Returns (relative-path, bytes) pairs; the CLI re-checks dest.starts_with(git_dir) after OS-level path resolution. - CLI cap: min(100 × compressed size, 1 GiB). Acceptance: 5 unit tests in relicario-core (traversal, absolute path, symlink, size bomb, happy path); existing CLI backup roundtrip tests remain green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1718,9 +1718,32 @@ fn cmd_backup_restore(input: PathBuf, target: PathBuf) -> Result<()> {
|
||||
|
||||
// .git/ history.
|
||||
if let Some(tar_bytes) = &unpacked.git_archive {
|
||||
let mut archive = tar::Archive::new(tar_bytes.as_slice());
|
||||
archive.unpack(target.join(".git"))
|
||||
.with_context(|| "failed to untar .git/")?;
|
||||
// Cap: 100× the compressed bundle size, or 1 GiB, whichever is lower.
|
||||
let cap = std::cmp::min(
|
||||
(tar_bytes.len() as u64).saturating_mul(100),
|
||||
relicario_core::DEFAULT_MAX_UNCOMPRESSED,
|
||||
);
|
||||
let entries = relicario_core::safe_unpack_git_archive(tar_bytes, cap)
|
||||
.with_context(|| "failed to safely unpack .git/ archive")?;
|
||||
let git_dir = target.join(".git");
|
||||
for (rel_path, body) in entries {
|
||||
let dest = git_dir.join(&rel_path);
|
||||
// Paranoid OS-level check even after textual validation in core.
|
||||
if !dest.starts_with(&git_dir) {
|
||||
anyhow::bail!(
|
||||
"tar entry {} resolved outside .git/ (path traversal blocked)",
|
||||
rel_path.display()
|
||||
);
|
||||
}
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| {
|
||||
format!("create parent {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
fs::write(&dest, &body).with_context(|| {
|
||||
format!("write {}", dest.display())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
// No history bundled — start a fresh git repo.
|
||||
let status = crate::helpers::git_command(&target, &["init"]).status()?;
|
||||
|
||||
Reference in New Issue
Block a user