Skip to main content

fluidattacks_core_git/
tar.rs

1use 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}