CryptoAPIDecryptor: don't size the descriptor array from an untrusted count - #1347
Merged
Merged
Conversation
pjfanning
force-pushed
the
cryptoapi-descriptor-count
branch
2 times, most recently
from
September 22, 2026 12:41
eca9a84 to
bbb0001
Compare
… count
getSummaryEntries reads the stream descriptor count as an unsigned 32-bit
field and used it directly as an array length, with no bound at all:
int encryptedStreamDescriptorCount = Math.toIntExact(leis.readUInt());
StreamDescriptorEntry[] entries =
new StreamDescriptorEntry[encryptedStreamDescriptorCount];
A crafted encrypted stream can therefore ask for a billion-element array from
a handful of input bytes.
Two changes:
* Reject a count the input cannot back. The stream is already read fully into
a byte[] and every entry occupies at least 18 bytes, so a declared count
larger than the bytes following it is necessarily bogus. This is derived
from the data rather than a fixed cap, so it cannot reject a well formed
file however many streams it carries.
* Collect the entries as they are parsed instead of pre-allocating, so the
memory used stays proportional to the data actually present.
entries was only consumed by an enhanced-for loop, so a List works in place of
the array. The read of the encrypted stream moves into a small helper so the
byte[] length is available for the bound - InputStream.available() is banned by
forbiddenapis, and its message points at exactly this pattern.
For reference, the encrypted summary stream carries the property set streams
(MS-OFFCRYPTO 2.3.5.4), so real files declare 2; the sample in
TestDocumentEncryption has 2 entries in 128 bytes.
Found while sweeping Math.toIntExact call sites after apache#1345.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pjfanning
force-pushed
the
cryptoapi-descriptor-count
branch
from
September 22, 2026 13:14
bbb0001 to
098a6ec
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CryptoAPIDecryptor.getSummaryEntriesreads the stream descriptor count as an unsigned 32-bit field and uses it directly as an array length, with no bound at all:A crafted encrypted stream can therefore ask for a billion-element
StreamDescriptorEntry[]from a handful of input bytes, before any entry data is read.What a real count looks like
The encrypted summary stream carries the property set streams (MS-OFFCRYPTO 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream), so a real file declares 2 —
SummaryInformationandDocumentSummaryInformation. Instrumenting the sample inTestDocumentEncryptionconfirms it: 2 entries in 128 bytes.Bound
Rather than pick a fixed cap — any number would be a guess, and a guess can reject a valid file — the bound is derived from the data. Every entry occupies at least 18 bytes (
streamOffset4 +streamSize4 +block2 +nameSize1 +flags1 +reserved24 + terminator 2), and the stream is already read fully into abyte[], so a declared count larger than the bytes following it is necessarily bogus:This cannot reject a well formed file however many streams it carries — the bytes have to be there either way.
The entries are then collected as they are parsed rather than pre-allocated, so memory stays proportional to the data actually present:
entrieswas only consumed by an enhanced-for loop, so aListdrops in for the array.Reading the encrypted stream moves into a small
readEncryptedStreamhelper so thebyte[]length is available for the bound.InputStream.available()is banned by forbiddenapis, and its message points at exactly this pattern — "use IOUtils.toByteArray to read the stream fully and then count the available bytes" — which is what the method already did.Context
Found while sweeping
Math.toIntExactcall sites after #1345. It was the only genuinely unbounded allocation the sweep turned up; the separateHwmfBitmapDibcleanup is #1346.Tests
:poi:test --tests 'org.apache.poi.poifs.crypt.*'and:poi-scratchpad:test --tests 'org.apache.poi.hslf.record.TestDocumentEncryption'pass (32 tests), andforbiddenApisMain/forbiddenApisTestare clean acrosspoi,poi-scratchpadandpoi-examples.🤖 Generated with Claude Code