fluidattacks_core_git/
tar.rs1use anyhow::{bail, Context, Result};
2use flate2::read::GzDecoder;
3use std::fs;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use tar::Archive;
7use tracing::{debug, info};
8
9pub fn extract(data: &[u8], dest_dir: &Path) -> Result<()> {
10 let abs_dest = dest_dir.canonicalize().context("resolving dest dir")?;
11
12 let decoder = GzDecoder::new(data);
13 let mut archive = Archive::new(decoder);
14
15 let mut entry_count = 0u64;
16 for entry_result in archive.entries().context("reading tar entries")? {
17 let mut entry = entry_result.context("reading tar entry")?;
18 entry_count = entry_count.saturating_add(1);
19
20 let path = entry.path().context("reading entry path")?.into_owned();
21
22 if entry_count <= 10 {
23 info!(
24 name = %path.display(),
25 kind = entry.header().entry_type().as_byte(),
26 size = entry.header().size().unwrap_or(0),
27 "tar entry"
28 );
29 }
30
31 match validate_entry(&path, &entry, &abs_dest)? {
32 EntryAction::Skip => {
33 debug!(name = %path.display(), "skipping tar entry");
34 continue;
35 }
36 EntryAction::Process => {}
37 }
38
39 let target = abs_dest.join(clean_path(&path));
40 write_entry(&mut entry, &target)?;
41 }
42
43 info!(total_entries = entry_count, "tar extraction complete");
44 Ok(())
45}
46
47fn write_entry(entry: &mut tar::Entry<impl Read>, target: &Path) -> Result<()> {
48 match entry.header().entry_type() {
49 tar::EntryType::Directory => {
50 fs::create_dir_all(target)
51 .with_context(|| format!("creating directory {}", target.display()))?;
52 }
53 tar::EntryType::Regular => {
54 if let Some(parent) = target.parent() {
55 fs::create_dir_all(parent)
56 .with_context(|| format!("creating parent dir for {}", target.display()))?;
57 }
58 let mode = entry.header().mode().unwrap_or(0o644);
59 let mut contents = Vec::new();
60 entry
61 .read_to_end(&mut contents)
62 .with_context(|| format!("reading file {}", target.display()))?;
63 fs::write(target, &contents)
64 .with_context(|| format!("writing file {}", target.display()))?;
65 #[cfg(unix)]
66 {
67 use std::os::unix::fs::PermissionsExt;
68 fs::set_permissions(target, fs::Permissions::from_mode(mode)).ok();
69 }
70 }
71 _ => {}
72 }
73 Ok(())
74}
75
76fn clean_path(path: &Path) -> PathBuf {
77 let mut clean = PathBuf::new();
78 for component in path.components() {
79 if let std::path::Component::Normal(c) = component {
80 clean.push(c);
81 }
82 }
83 clean
84}
85
86enum EntryAction {
87 Process,
88 Skip,
89}
90
91fn validate_entry(
92 path: &Path,
93 entry: &tar::Entry<impl Read>,
94 dest_dir: &Path,
95) -> Result<EntryAction> {
96 let path_str = path.to_string_lossy();
97
98 if path.is_absolute() {
99 bail!("tar contains absolute path: {path_str}");
100 }
101
102 if path_str.contains("..") {
103 bail!("tar contains path traversal: {path_str}");
104 }
105
106 let target = dest_dir.join(clean_path(path));
107 if !target.starts_with(dest_dir) {
108 bail!("tar entry escapes destination: {path_str}");
109 }
110
111 let entry_type = entry.header().entry_type();
112 if entry_type == tar::EntryType::Symlink || entry_type == tar::EntryType::Link {
113 return Ok(EntryAction::Skip);
114 }
115
116 Ok(EntryAction::Process)
117}