Skip to main content

fluidattacks_core/
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 += 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
41        match entry.header().entry_type() {
42            tar::EntryType::Directory => {
43                fs::create_dir_all(&target)
44                    .with_context(|| format!("creating directory {}", target.display()))?;
45            }
46            tar::EntryType::Regular => {
47                if let Some(parent) = target.parent() {
48                    fs::create_dir_all(parent)
49                        .with_context(|| format!("creating parent dir for {}", target.display()))?;
50                }
51                let mode = entry.header().mode().unwrap_or(0o644);
52                let mut contents = Vec::new();
53                entry
54                    .read_to_end(&mut contents)
55                    .with_context(|| format!("reading file {}", target.display()))?;
56                fs::write(&target, &contents)
57                    .with_context(|| format!("writing file {}", target.display()))?;
58                #[cfg(unix)]
59                {
60                    use std::os::unix::fs::PermissionsExt;
61                    fs::set_permissions(&target, fs::Permissions::from_mode(mode)).ok();
62                }
63            }
64            _ => {
65                continue;
66            }
67        }
68    }
69
70    info!(total_entries = entry_count, "tar extraction complete");
71    Ok(())
72}
73
74fn clean_path(path: &Path) -> PathBuf {
75    let mut clean = PathBuf::new();
76    for component in path.components() {
77        match component {
78            std::path::Component::Normal(c) => clean.push(c),
79            std::path::Component::CurDir => {}
80            _ => {}
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}