#[cfg(any(feature = "archives", feature = "hwpx"))]
use std::io::{Read, Seek};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
#[serde(default)]
pub struct SecurityLimits {
pub max_archive_size: usize,
pub max_compression_ratio: usize,
pub max_files_in_archive: usize,
pub max_nesting_depth: usize,
pub max_entity_length: usize,
pub max_content_size: usize,
pub max_iterations: usize,
pub max_xml_depth: usize,
pub max_table_cells: usize,
}
impl Default for SecurityLimits {
fn default() -> Self {
Self {
max_archive_size: 500 * 1024 * 1024,
max_compression_ratio: 100,
max_files_in_archive: 10_000,
max_nesting_depth: 1024,
max_entity_length: 1024 * 1024,
max_content_size: 100 * 1024 * 1024,
max_iterations: 10_000_000,
max_xml_depth: 1024,
max_table_cells: 100_000,
}
}
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone)]
pub enum SecurityError {
ZipBombDetected {
compressed_size: u64,
uncompressed_size: u64,
ratio: f64,
},
ArchiveTooLarge {
size: u64,
max: usize,
},
TooManyFiles {
count: usize,
max: usize,
},
NestingTooDeep {
depth: usize,
max: usize,
},
ContentTooLarge {
size: usize,
max: usize,
},
EntityTooLong {
length: usize,
max: usize,
},
TooManyIterations {
count: usize,
max: usize,
},
XmlDepthExceeded {
depth: usize,
max: usize,
},
TooManyCells {
cells: usize,
max: usize,
},
}
impl std::fmt::Display for SecurityError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
SecurityError::ZipBombDetected {
compressed_size,
uncompressed_size,
ratio,
} => {
write!(
f,
"Potential ZIP bomb detected: compressed {}B -> uncompressed {}B (ratio: {:.1}:1)",
compressed_size, uncompressed_size, ratio
)
}
SecurityError::ArchiveTooLarge { size, max } => {
write!(f, "Archive too large: {} bytes (max: {} bytes)", size, max)
}
SecurityError::TooManyFiles { count, max } => {
write!(f, "Archive has too many files: {} (max: {})", count, max)
}
SecurityError::NestingTooDeep { depth, max } => {
write!(f, "Nesting too deep: {} levels (max: {})", depth, max)
}
SecurityError::ContentTooLarge { size, max } => {
write!(f, "Content too large: {} bytes (max: {} bytes)", size, max)
}
SecurityError::EntityTooLong { length, max } => {
write!(f, "Entity too long: {} chars (max: {})", length, max)
}
SecurityError::TooManyIterations { count, max } => {
write!(f, "Too many iterations: {} (max: {})", count, max)
}
SecurityError::XmlDepthExceeded { depth, max } => {
write!(f, "XML depth exceeded: {} (max: {})", depth, max)
}
SecurityError::TooManyCells { cells, max } => {
write!(f, "Too many table cells: {} (max: {})", cells, max)
}
}
}
}
impl std::error::Error for SecurityError {}
#[cfg(any(feature = "archives", feature = "hwpx"))]
#[cfg_attr(alef, alef(skip))]
pub struct ZipBombValidator {
limits: SecurityLimits,
}
#[cfg(any(feature = "archives", feature = "hwpx"))]
impl ZipBombValidator {
pub(crate) fn new(limits: SecurityLimits) -> Self {
Self { limits }
}
pub(crate) fn validate<R: Read + Seek>(&self, archive: &mut zip::ZipArchive<R>) -> Result<(), SecurityError> {
let file_count = archive.len();
if file_count > self.limits.max_files_in_archive {
return Err(SecurityError::TooManyFiles {
count: file_count,
max: self.limits.max_files_in_archive,
});
}
let mut total_uncompressed: u64 = 0;
let mut total_compressed: u64 = 0;
for i in 0..file_count {
if let Ok(file) = archive.by_index(i) {
let compressed_size = file.compressed_size();
let uncompressed_size = file.size();
total_uncompressed += uncompressed_size;
total_compressed += compressed_size;
if compressed_size > 0 && uncompressed_size > 0 {
let ratio = uncompressed_size as f64 / compressed_size as f64;
if ratio > self.limits.max_compression_ratio as f64 {
return Err(SecurityError::ZipBombDetected {
compressed_size,
uncompressed_size,
ratio,
});
}
}
}
}
if total_uncompressed > self.limits.max_archive_size as u64 {
return Err(SecurityError::ArchiveTooLarge {
size: total_uncompressed,
max: self.limits.max_archive_size,
});
}
if total_compressed > 0 {
let ratio = total_uncompressed as f64 / total_compressed as f64;
if ratio > self.limits.max_compression_ratio as f64 {
return Err(SecurityError::ZipBombDetected {
compressed_size: total_compressed,
uncompressed_size: total_uncompressed,
ratio,
});
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub(crate) struct StringGrowthValidator {
max_size: usize,
current_size: usize,
}
impl StringGrowthValidator {
pub(crate) fn new(max_size: usize) -> Self {
Self {
max_size,
current_size: 0,
}
}
pub(crate) fn check_append(&mut self, len: usize) -> Result<(), SecurityError> {
self.current_size = self.current_size.saturating_add(len);
if self.current_size > self.max_size {
Err(SecurityError::ContentTooLarge {
size: self.current_size,
max: self.max_size,
})
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct IterationValidator {
max_iterations: usize,
current_count: usize,
}
impl IterationValidator {
pub(crate) fn new(max_iterations: usize) -> Self {
Self {
max_iterations,
current_count: 0,
}
}
pub(crate) fn check_iteration(&mut self) -> Result<(), SecurityError> {
self.current_count = self.current_count.saturating_add(1);
if self.current_count > self.max_iterations {
Err(SecurityError::TooManyIterations {
count: self.current_count,
max: self.max_iterations,
})
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DepthValidator {
max_depth: usize,
current_depth: usize,
}
impl DepthValidator {
pub(crate) fn new(max_depth: usize) -> Self {
Self {
max_depth,
current_depth: 0,
}
}
pub(crate) fn push(&mut self) -> Result<(), SecurityError> {
self.current_depth = self.current_depth.saturating_add(1);
if self.current_depth > self.max_depth {
Err(SecurityError::NestingTooDeep {
depth: self.current_depth,
max: self.max_depth,
})
} else {
Ok(())
}
}
pub(crate) fn pop(&mut self) {
if self.current_depth > 0 {
self.current_depth -= 1;
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct EntityValidator {
max_length: usize,
}
impl EntityValidator {
pub(crate) fn new(max_length: usize) -> Self {
Self { max_length }
}
pub(crate) fn validate(&self, content: &str) -> Result<(), SecurityError> {
if content.len() > self.max_length {
Err(SecurityError::EntityTooLong {
length: content.len(),
max: self.max_length,
})
} else {
Ok(())
}
}
#[cfg(any(feature = "xml", feature = "office"))]
pub(crate) fn check_attr(&self, _name: &str, value: &str) -> Result<(), SecurityError> {
self.validate(value)
}
}
#[derive(Debug, Clone)]
pub(crate) struct TableValidator {
max_cells: usize,
current_cells: usize,
}
impl TableValidator {
pub(crate) fn new(max_cells: usize) -> Self {
Self {
max_cells,
current_cells: 0,
}
}
pub(crate) fn add_cells(&mut self, count: usize) -> Result<(), SecurityError> {
self.current_cells = self.current_cells.saturating_add(count);
if self.current_cells > self.max_cells {
Err(SecurityError::TooManyCells {
cells: self.current_cells,
max: self.max_cells,
})
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct SecurityBudget {
pub(crate) depth: DepthValidator,
pub(crate) iteration: IterationValidator,
pub(crate) entity: EntityValidator,
pub(crate) growth: StringGrowthValidator,
pub(crate) table: TableValidator,
}
impl SecurityBudget {
pub(crate) fn from_limits(limits: &SecurityLimits) -> Self {
Self {
depth: DepthValidator::new(limits.max_xml_depth.max(limits.max_nesting_depth)),
iteration: IterationValidator::new(limits.max_iterations),
entity: EntityValidator::new(limits.max_entity_length),
growth: StringGrowthValidator::new(limits.max_content_size),
table: TableValidator::new(limits.max_table_cells),
}
}
pub(crate) fn from_config(config: &crate::core::config::ExtractionConfig) -> Self {
let owned: SecurityLimits;
let limits: &SecurityLimits = match config.security_limits.as_ref() {
Some(l) => l,
None => {
owned = SecurityLimits::default();
&owned
}
};
Self::from_limits(limits)
}
#[cfg(any(feature = "xml", all(test, feature = "office")))]
pub(crate) fn with_defaults() -> Self {
Self::from_limits(&SecurityLimits::default())
}
pub(crate) fn step(&mut self) -> Result<(), SecurityError> {
self.iteration.check_iteration()
}
pub(crate) fn enter(&mut self) -> Result<(), SecurityError> {
self.depth.push()
}
pub(crate) fn leave(&mut self) {
self.depth.pop();
}
pub(crate) fn account_text(&mut self, len: usize) -> Result<(), SecurityError> {
self.growth.check_append(len)
}
#[cfg(any(feature = "xml", feature = "office"))]
pub(crate) fn check_attr(&self, name: &str, value: &str) -> Result<(), SecurityError> {
self.entity.check_attr(name, value)
}
pub(crate) fn check_entity(&self, value: &str) -> Result<(), SecurityError> {
self.entity.validate(value)
}
pub(crate) fn add_cells(&mut self, count: usize) -> Result<(), SecurityError> {
self.table.add_cells(count)
}
}
#[allow(dead_code)]
pub(crate) fn has_path_traversal(path_str: &str) -> bool {
use std::path::{Component, Path};
Path::new(path_str).components().any(|c| c == Component::ParentDir)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_limits() {
let limits = SecurityLimits::default();
assert_eq!(limits.max_archive_size, 500 * 1024 * 1024);
assert_eq!(limits.max_nesting_depth, 1024);
assert_eq!(limits.max_entity_length, 1024 * 1024);
}
#[test]
fn test_string_growth_validator_basic() {
let mut v = StringGrowthValidator::new(100);
assert!(v.check_append(50).is_ok());
assert_eq!(v.current_size, 50);
assert!(v.check_append(50).is_ok());
assert_eq!(v.current_size, 100);
assert!(matches!(
v.check_append(1),
Err(SecurityError::ContentTooLarge { size: 101, max: 100 })
));
}
#[test]
fn test_string_growth_validator_saturates_on_overflow() {
let mut v = StringGrowthValidator::new(usize::MAX - 10);
assert!(v.check_append(usize::MAX).is_err(), "saturating add cannot wrap");
}
#[test]
fn test_iteration_validator_basic() {
let mut v = IterationValidator::new(3);
assert!(v.check_iteration().is_ok());
assert!(v.check_iteration().is_ok());
assert!(v.check_iteration().is_ok());
assert!(matches!(
v.check_iteration(),
Err(SecurityError::TooManyIterations { count: 4, max: 3 })
));
}
#[test]
fn test_depth_validator_push_pop() {
let mut v = DepthValidator::new(3);
assert!(v.push().is_ok());
assert!(v.push().is_ok());
assert!(v.push().is_ok());
assert_eq!(v.current_depth, 3);
assert!(matches!(
v.push(),
Err(SecurityError::NestingTooDeep { depth: 4, max: 3 })
));
v.pop();
assert_eq!(v.current_depth, 3);
}
#[test]
fn test_depth_validator_pop_saturates_at_zero() {
let mut v = DepthValidator::new(10);
v.pop();
v.pop();
assert_eq!(v.current_depth, 0, "underflow is impossible");
}
#[test]
fn test_entity_validator() {
let v = EntityValidator::new(10);
assert!(v.validate("short").is_ok());
assert!(v.validate("0123456789").is_ok());
assert!(matches!(
v.validate("01234567890"),
Err(SecurityError::EntityTooLong { length: 11, max: 10 })
));
#[cfg(any(feature = "xml", feature = "office"))]
{
assert!(v.check_attr("href", "http://x").is_ok());
assert!(v.check_attr("data", &"x".repeat(50)).is_err());
}
}
#[test]
fn test_table_validator() {
let mut v = TableValidator::new(10);
assert!(v.add_cells(5).is_ok());
assert_eq!(v.current_cells, 5);
assert!(v.add_cells(5).is_ok());
assert_eq!(v.current_cells, 10);
assert!(matches!(
v.add_cells(1),
Err(SecurityError::TooManyCells { cells: 11, max: 10 })
));
}
#[test]
fn test_path_traversal_detected_in_simple_dotdot() {
assert!(has_path_traversal("../etc/passwd"));
}
#[test]
fn test_path_traversal_detected_in_middle_of_path() {
assert!(has_path_traversal("word/images/../../etc/passwd"));
}
#[test]
fn test_path_traversal_detected_at_end() {
assert!(has_path_traversal("word/images/.."));
}
#[test]
fn test_normal_path_not_flagged() {
assert!(!has_path_traversal("word/images/photo.png"));
}
#[test]
fn test_empty_path_not_flagged() {
assert!(!has_path_traversal(""));
}
#[test]
fn test_dotdot_in_filename_not_flagged() {
assert!(!has_path_traversal("images/1..2.png"));
}
#[test]
fn test_absolute_path_without_traversal_not_flagged() {
assert!(!has_path_traversal("/usr/local/share/doc.pdf"));
}
}