Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
48 changes: 48 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,51 @@ 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 {
path.push_str(&format!("/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}"
);
}
36 changes: 33 additions & 3 deletions jans-cedarling/cedarling/src/jwt/status_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,26 @@ pub(super) struct StatusList {
}

impl StatusList {
/// Hard safety limit on decompressed status list size to prevent zip-bombs/OOM.
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 compressed = BASE64_URL_SAFE_NO_PAD.decode(encoded)?;
let decoder = ZlibDecoder::new(compressed.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 +366,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