diff --git a/jans-cedarling/cedarling/src/common/policy_store/errors.rs b/jans-cedarling/cedarling/src/common/policy_store/errors.rs index 374a0a022a3..43392728617 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/errors.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/errors.rs @@ -146,6 +146,10 @@ pub(crate) enum PolicyStoreError { #[source] source: std::io::Error, }, + + /// Maximum directory recursion depth exceeded + #[error("Maximum directory recursion depth ({max_depth}) exceeded at '{path}'")] + MaxDepthExceeded { path: String, max_depth: usize }, } /// Details about Cedar parsing errors. diff --git a/jans-cedarling/cedarling/src/common/policy_store/loader.rs b/jans-cedarling/cedarling/src/common/policy_store/loader.rs index 5c6e7189718..98794cf31dd 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/loader.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/loader.rs @@ -513,6 +513,10 @@ impl DefaultPolicyStoreLoader { Ok(issuers) } + /// Maximum directory recursion depth to prevent stack overflow from deeply + /// nested `.cjar` archives or filesystem trees. + const MAX_RECURSION_DEPTH: usize = 64; + /// Helper: Load all .cedar files from a directory, recursively scanning subdirectories. fn load_cedar_files( &self, @@ -520,7 +524,7 @@ impl DefaultPolicyStoreLoader { _file_type: &str, ) -> Result, PolicyStoreError> { let mut files = Vec::new(); - self.load_cedar_files_recursive(dir, &mut files)?; + self.load_cedar_files_recursive(dir, &mut files, 0)?; Ok(files) } @@ -529,7 +533,15 @@ impl DefaultPolicyStoreLoader { &self, dir: &str, files: &mut Vec, + depth: usize, ) -> Result<(), PolicyStoreError> { + if depth > Self::MAX_RECURSION_DEPTH { + return Err(PolicyStoreError::MaxDepthExceeded { + path: dir.to_string(), + max_depth: Self::MAX_RECURSION_DEPTH, + }); + } + let entries = self.vfs .read_dir(dir) @@ -541,7 +553,7 @@ impl DefaultPolicyStoreLoader { for entry in entries { if entry.is_dir { // Recursively scan subdirectories - self.load_cedar_files_recursive(&entry.path, files)?; + self.load_cedar_files_recursive(&entry.path, files, depth + 1)?; } else { // Validate .cedar extension if !entry.name.to_lowercase().ends_with(".cedar") { diff --git a/jans-cedarling/cedarling/src/common/policy_store/loader_tests.rs b/jans-cedarling/cedarling/src/common/policy_store/loader_tests.rs index 3a0f89e6a7a..462b0355760 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/loader_tests.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/loader_tests.rs @@ -2159,3 +2159,52 @@ fn test_archive_shared_namespace_full_pipeline() { "Archive schema should contain App::Admin; got: {type_names:?}" ); } + +#[test] +fn test_max_recursion_depth_exceeded() { + let vfs = MemoryVfs::new(); + + vfs.create_file( + "metadata.json", + br#"{ + "cedar_version": "4.4.0", + "policy_store": { + "id": "abcdef1234567890", + "name": "Deep Nesting Test", + "version": "1.0.0" + } + }"#, + ) + .unwrap(); + + vfs.create_file( + "schema.cedarschema", + b"namespace App { entity User; entity Resource; action \"read\" appliesTo { principal: [User], resource: [Resource] }; }", + ) + .unwrap(); + + // Build a directory tree deeper than MAX_RECURSION_DEPTH (64). + // Place a .cedar file at the bottom so the only failure path is the depth check. + let depth = 66; + let mut path = String::from("policies"); + for i in 0..depth { + use std::fmt::Write; + let _ = write!(path, "/level{i}"); + } + let file_path = format!("{path}/deep.cedar"); + vfs.create_file( + &file_path, + b"permit(principal, action, resource);", + ) + .unwrap(); + + let loader = DefaultPolicyStoreLoader::new(vfs); + let result = loader.load_directory(".", true); + + let err = result.expect_err("Should fail with MaxDepthExceeded"); + let err_msg = err.to_string(); + assert!( + err_msg.contains("Maximum directory recursion depth"), + "Error should mention max depth, got: {err_msg}" + ); +} diff --git a/jans-cedarling/cedarling/src/jwt/status_list.rs b/jans-cedarling/cedarling/src/jwt/status_list.rs index df5a3404716..fb8cd9f32c0 100644 --- a/jans-cedarling/cedarling/src/jwt/status_list.rs +++ b/jans-cedarling/cedarling/src/jwt/status_list.rs @@ -35,11 +35,27 @@ pub(super) struct StatusList { } impl StatusList { + /// Maximum allowed decompressed size for a status list to prevent zip-bombs/OOM. + /// Default is 10 MB. + const MAX_DECOMPRESSED_SIZE: u64 = 10 * 1024 * 1024; + pub(super) fn parse(encoded: &str, bits: u8) -> Result { let list = BASE64_URL_SAFE_NO_PAD.decode(encoded)?; - let mut decoder = ZlibDecoder::new(list.as_slice()); + let decoder = ZlibDecoder::new(list.as_slice()); + + let mut bounded_decoder = decoder.take(Self::MAX_DECOMPRESSED_SIZE); + let mut list = Vec::new(); - decoder.read_to_end(&mut list)?; + bounded_decoder.read_to_end(&mut list)?; + + // Check if there is still more data in the decoder stream + let mut extra = [0u8; 1]; + if bounded_decoder.into_inner().read(&mut extra)? != 0 { + return Err(ParseStatusListError::DecompressedSizeExceeded( + Self::MAX_DECOMPRESSED_SIZE, + )); + } + Ok(Self { bit_size: bits.try_into()?, list, @@ -351,4 +367,19 @@ mod test { } ); } + #[test] + fn prevent_zip_bomb() { + let size = 10 * 1024 * 1024 + 1; // 10 MB + 1 byte + let malicious_payload = vec![0u8; size]; + let encoded = compress_and_encode(&malicious_payload); + + let result = StatusList::parse(&encoded, 1); + assert!( + matches!( + result, + Err(ParseStatusListError::DecompressedSizeExceeded(_)) + ), + "Oversized payload must return DecompressedSizeExceeded, but got {result:?}" + ); + } } diff --git a/jans-cedarling/cedarling/src/jwt/status_list/error.rs b/jans-cedarling/cedarling/src/jwt/status_list/error.rs index b56753c6062..d3c4e44240e 100644 --- a/jans-cedarling/cedarling/src/jwt/status_list/error.rs +++ b/jans-cedarling/cedarling/src/jwt/status_list/error.rs @@ -26,6 +26,8 @@ pub enum ParseStatusListError { JwtInvalidBitsType(serde_json::Value), #[error("failed to convert bits value: {0}")] BitsConversion(#[from] std::num::TryFromIntError), + #[error("decompressed status list exceeds the maximum allowed size of {0} bytes")] + DecompressedSizeExceeded(u64), } #[derive(Debug, Error)]