From 2cf2c40075ac04af99a0ed2a54ebfdc33f4c6efd Mon Sep 17 00:00:00 2001 From: "William T. Nelson" <35801+wtn@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:57:22 -0600 Subject: [PATCH] add recursion depth limit to prevent stack overflow Co-authored-by: Claude --- lib/cfpropertylist/rbBinaryCFPropertyList.rb | 25 +++++++++++--------- test/test_array.rb | 13 ++++++++++ test/test_dictionary.rb | 13 ++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/cfpropertylist/rbBinaryCFPropertyList.rb b/lib/cfpropertylist/rbBinaryCFPropertyList.rb index da84b63..7703e29 100644 --- a/lib/cfpropertylist/rbBinaryCFPropertyList.rb +++ b/lib/cfpropertylist/rbBinaryCFPropertyList.rb @@ -5,6 +5,8 @@ module CFPropertyList # Binary PList parser class class Binary + MAX_DEPTH = 512 + # Read a binary plist file def load(opts) @unique_table = {} @@ -264,7 +266,7 @@ def unpack_with_size(nbytes, buff) end # Read an binary array value, including contained objects - def read_binary_array(fname,fd,length) + def read_binary_array(fname,fd,length,depth=0) ary = [] # first: read object refs @@ -274,7 +276,7 @@ def read_binary_array(fname,fd,length) # now: read objects 0.upto(length-1) do |i| - object = read_binary_object_at(fname,fd,objects[i]) + object = read_binary_object_at(fname,fd,objects[i],depth+1) ary.push object end end @@ -284,7 +286,7 @@ def read_binary_array(fname,fd,length) protected :read_binary_array # Read a dictionary value, including contained objects - def read_binary_dict(fname,fd,length) + def read_binary_dict(fname,fd,length,depth=0) dict = {} # first: read keys @@ -298,8 +300,8 @@ def read_binary_dict(fname,fd,length) # read real keys and objects 0.upto(length-1) do |i| - key = read_binary_object_at(fname,fd,keys[i]) - object = read_binary_object_at(fname,fd,objects[i]) + key = read_binary_object_at(fname,fd,keys[i],depth+1) + object = read_binary_object_at(fname,fd,objects[i],depth+1) dict[key.value] = object end end @@ -310,7 +312,7 @@ def read_binary_dict(fname,fd,length) # Read an object type byte, decode it and delegate to the correct # reader function - def read_binary_object(fname,fd) + def read_binary_object(fname,fd,depth=0) # first: read the marker byte buff = fd.read(1) @@ -321,7 +323,7 @@ def read_binary_object(fname,fd) object_type = buff[0][0].chr if(object_type != "0" && object_length == 15) then - object_length = read_binary_object(fname,fd) + object_length = read_binary_object(fname,fd,depth) object_length = object_length.value end @@ -343,18 +345,19 @@ def read_binary_object(fname,fd) when '8' CFUid.new(read_binary_int(fname, fd, object_length).value) when 'a' # array - read_binary_array(fname,fd,object_length) + read_binary_array(fname,fd,object_length,depth) when 'd' # dictionary - read_binary_dict(fname,fd,object_length) + read_binary_dict(fname,fd,object_length,depth) end end protected :read_binary_object # Read an object type byte at position $pos, decode it and delegate to the correct reader function - def read_binary_object_at(fname,fd,pos) + def read_binary_object_at(fname,fd,pos,depth=0) + raise CFFormatError.new("#{fname}: Maximum depth exceeded") if depth > MAX_DEPTH position = @offsets[pos] fd.seek(position,IO::SEEK_SET) - read_binary_object(fname,fd) + read_binary_object(fname,fd,depth) end protected :read_binary_object_at diff --git a/test/test_array.rb b/test/test_array.rb index 39941ed..956a0a9 100644 --- a/test/test_array.rb +++ b/test/test_array.rb @@ -45,4 +45,17 @@ def test_big_array assert_equal raw_xml('big_array'), plist.to_str(CFPropertyList::List::FORMAT_XML, :formatted => false) assert_equal raw_binary('big_array'), plist.to_str(CFPropertyList::List::FORMAT_BINARY) end + + def test_deeply_nested_array_binary + nested = "x" + 2000.times { nested = [nested] } + + plist = CFPropertyList::List.new + plist.value = CFPropertyList.guess(nested) + binary = plist.to_str(CFPropertyList::List::FORMAT_BINARY) + + assert_raises CFFormatError do + CFPropertyList::List.new(:data => binary) + end + end end diff --git a/test/test_dictionary.rb b/test/test_dictionary.rb index 9ab3708..63570e6 100644 --- a/test/test_dictionary.rb +++ b/test/test_dictionary.rb @@ -77,4 +77,17 @@ def test_empty_key_with_rexml ensure CFPropertyList::List.parsers = orig_parsers end + + def test_deeply_nested_dict_binary + nested = "x" + 2000.times { |i| nested = {"k#{i}" => nested} } + + plist = CFPropertyList::List.new + plist.value = CFPropertyList.guess(nested) + binary = plist.to_str(CFPropertyList::List::FORMAT_BINARY) + + assert_raises CFFormatError do + CFPropertyList::List.new(:data => binary) + end + end end