verso/library/
epub_guard.rs1use std::path::Path;
2use thiserror::Error;
3
4pub struct Limits {
5 pub max_decompressed_bytes: u64,
6 pub max_entry_bytes: u64,
7 pub max_entries: usize,
8}
9impl Default for Limits {
10 fn default() -> Self {
11 Self {
12 max_decompressed_bytes: 256 * 1024 * 1024,
13 max_entry_bytes: 16 * 1024 * 1024,
14 max_entries: 10_000,
15 }
16 }
17}
18
19#[derive(Debug, Error)]
20pub enum GuardError {
21 #[error("path traversal attempt in zip entry: {0}")]
22 PathTraversal(String),
23 #[error("archive entry count {0} exceeds limit {1}")]
24 TooManyEntries(usize, usize),
25 #[error("entry {0} size {1} exceeds per-entry limit {2}")]
26 EntryTooLarge(String, u64, u64),
27 #[error("total decompressed size {0} exceeds limit {1}")]
28 TotalTooLarge(u64, u64),
29 #[error("symlink entry not allowed: {0}")]
30 Symlink(String),
31 #[error("io: {0}")]
32 Io(#[from] std::io::Error),
33 #[error("zip: {0}")]
34 Zip(#[from] zip::result::ZipError),
35}
36
37pub fn validate_archive(path: &Path, limits: Limits) -> Result<(), GuardError> {
39 let file = std::fs::File::open(path)?;
40 let mut archive = zip::ZipArchive::new(file)?;
41
42 if archive.len() > limits.max_entries {
43 return Err(GuardError::TooManyEntries(
44 archive.len(),
45 limits.max_entries,
46 ));
47 }
48
49 let mut total: u64 = 0;
50 for i in 0..archive.len() {
51 let entry = archive.by_index(i)?;
52 let name = entry.name().to_string();
53
54 if name.contains("..") || name.starts_with('/') || name.starts_with('\\') {
55 return Err(GuardError::PathTraversal(name));
56 }
57 if is_symlink(&entry) {
58 return Err(GuardError::Symlink(name));
59 }
60 if entry.size() > limits.max_entry_bytes {
61 return Err(GuardError::EntryTooLarge(
62 name,
63 entry.size(),
64 limits.max_entry_bytes,
65 ));
66 }
67 total = total.saturating_add(entry.size());
68 if total > limits.max_decompressed_bytes {
69 return Err(GuardError::TotalTooLarge(
70 total,
71 limits.max_decompressed_bytes,
72 ));
73 }
74 }
75 Ok(())
76}
77
78fn is_symlink(entry: &zip::read::ZipFile<'_>) -> bool {
79 entry
82 .unix_mode()
83 .map(|m| (m & 0o170000) == 0o120000)
84 .unwrap_or(false)
85}