Skip to content
Open
4 changes: 4 additions & 0 deletions jans-cedarling/cedarling/src/common/policy_store/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions jans-cedarling/cedarling/src/common/policy_store/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,14 +513,18 @@ impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
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,
dir: &str,
_file_type: &str,
) -> Result<Vec<PolicyFile>, 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)
}

Expand All @@ -529,7 +533,15 @@ impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
&self,
dir: &str,
files: &mut Vec<PolicyFile>,
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)
Expand All @@ -541,7 +553,7 @@ impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
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") {
Expand Down
49 changes: 49 additions & 0 deletions jans-cedarling/cedarling/src/common/policy_store/loader_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}
35 changes: 33 additions & 2 deletions jans-cedarling/cedarling/src/jwt/status_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, ParseStatusListError> {
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,
Expand Down Expand Up @@ -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:?}"
);
}
}
2 changes: 2 additions & 0 deletions jans-cedarling/cedarling/src/jwt/status_list/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading