PBM IndexedDB encrypted stream exposes unzeroed pre-encryption buffer tail bytes to the content process through EncryptedFileInputStreamParams
Categories
(Core :: Storage: IndexedDB, defect, P3)
Tracking
()
People
(Reporter: tomoya2517, Assigned: abienner)
Details
(Keywords: csectype-disclosure, reporter-external, sec-moderate, Whiteboard: [client-bounty-form][adv-main153+][adv-ESR140.13+])
Attachments
(4 files)
|
12.37 KB,
text/x-patch
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
Platform: Desktop
Version: Firefox Nightly 153.0a1 (commit 6e5c51c815fd4f0bb04bdc49a5124a6721c5db3a)
Summary
When a Private Browsing Mode IndexedDB Blob is sent from the parent process to a content process, Firefox serializes enough encrypted-stream material for the content side to reconstruct the stream. A compromised content process can use those IPC parameters to read the raw encrypted base stream and decrypt the rounded final-block payload, including the hidden bytes after ActualPayloadLength().
Normal Blob / IndexedDB readback returns only the logical Blob contents, so ordinary web content does not see those bytes. The issue is that the IPC-delivered stream material lets a compromised content process recover up to 15 bytes beyond the logical final payload of each affected partial encrypted file block.
Steps to Reproduce
The attached patch has three proof-only pieces:
- A content-process probe in
dom/quota/DecryptingInputStream_impl.hatDecryptingInputStream::Deserialize(). The probe runs in the patched content process and uses only the receivedEncryptedFileInputStreamParamskey and the deserialized raw base stream to decrypt the final encrypted block. - A proof seed in
dom/quota/EncryptingOutputStream_impl.h. It fills the newly allocated plaintext buffer with0xa0 + (offset & 0x0f)before the logical write. A one-byte Blob overwrites onlymBuffer[0], somBuffer[1..15]remains as known pre-encryption buffer content. - A browser-chrome test
dom/indexedDB/test/browser_encrypted_tail_probe.jsthat opens a private window, stores a one-byte Blob in IndexedDB, then reads the Blob back to force the parent-to-childRemoteLazyInputStreampath.
The one-byte Blob creates a partial final encrypted block for the real IndexedDB encrypted stream block size:
encrypted block size: 4096
actual-length header block: 16
NSS cipher prefix: 32
cipher rounding unit: 16
max payload per block: 4048
test payload: 1
The test payload is a single byte, 0xd0. The maximum logical payload per block is 4096 - 16 - 32 = 4048. The final block has ActualPayloadLength() == 1, and that one-byte payload is encrypted as a 16-byte rounded cipher input, so any bytes at offsets 1 through 15 after decryption are bytes that came from the rounded cipher input, not from the logical Blob length.
Apply the patch, build, and run
git apply repro.patch
./mach build
./mach test --headless dom/indexedDB/test/browser_encrypted_tail_probe.js
Security Impact
This is a sandbox-boundary information disclosure from the parent process to a compromised content process.
A compromised content process can recover up to 15 bytes beyond the logical final payload of each affected Private Browsing Mode IndexedDB encrypted file stream. These bytes are not exposed through normal Blob / IndexedDB readback and should remain outside the content process's observable data.
The disclosure is small per partial encrypted file block, but the affected bytes come from parent-process encryption-buffer memory and were never part of the web-visible Blob data. This crosses the intended process boundary for Private Browsing Mode encrypted Blob storage.
Technical Analysis
Bug 1864123 (CVE-2023-6865) fix randomized the unused encrypted payload area:
EncryptingOutputStream_impl.h:219
const auto payload = mEncryptedBlock->MutablePayload();
const auto unusedPayload = payload.From(mNextByte);
mRandomGenerator->GenerateRandomBytesInto(unusedPayload.Elements(),
unusedPayload.Length());
This protects the encrypted block area after the logical payload, but only until the cipher overwrites the rounded prefix of that area. The subsequent cipher call still uses the plaintext buffer rounded up to the basic block size:
EncryptingOutputStream_impl.h:244
nsresult rv = mCipherStrategy.Cipher(
mEncryptedBlock->MutableCipherPrefix(),
mozilla::Span(reinterpret_cast<uint8_t*>(mBuffer.Elements()),
((mNextByte + (CipherStrategy::BasicBlockSize - 1)) /
CipherStrategy::BasicBlockSize) *
CipherStrategy::BasicBlockSize),
mEncryptedBlock->MutablePayload());
For the NSS stream cipher strategy, BasicBlockSize == 16, so a final payload of 1 byte encrypts 16 bytes of mBuffer. mBuffer[1..15] is not written by that logical payload and is not zero-filled before encryption. NSSCipherStrategy.h:26
The decrypting side also processes the rounded payload:
ConstSpan Payload() const {
return WholeBlock()
.SplitAt(CipherPrefixOffset() + CipherPrefixLength)
.second.First(RoundedUpToBasicBlockSize(ActualPayloadLength()));
}
The normal ReadSegments() path only returns ActualPayloadLength(), which is why ordinary Blob readback is truncated. DecryptingInputStream_impl.h:199
But the parent-to-child lazy stream IPC path exposes the materials needed to decrypt the rounded payload manually.
DecryptingInputStream::Serialize() sends the raw base stream and key:
DecryptingInputStream_impl.h:542
mozilla::ipc::InputStreamHelper::SerializeInputStream(
*mBaseStream, encryptedFileInputStreamParams.inputStreamParams(),
aMaxSize, aSizeUsed);
encryptedFileInputStreamParams.key().AppendElements(
mCipherStrategy.SerializeKey(*mKey));
encryptedFileInputStreamParams.blockSize() = *mBlockSize;
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 1•3 months ago
|
||
Comment 3•3 months ago
|
||
Updated•3 months ago
|
| Assignee | ||
Comment 4•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D309603
Updated•3 months ago
|
Comment 5•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: sec-moderate bug which could leak information on a compromised content process
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Small change, well scoped.
- String changes made/needed?: N/A
- Is Android affected?: yes
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 7•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D309603
Updated•3 months ago
|
Comment 8•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: sec-moderate bug which could leak information on a compromised content process
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Small change, well scoped.
- String changes made/needed?: N/A
- Is Android affected?: yes
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 10•2 months ago
|
||
as presented, this is not impactful enough to qualify for a bounty that starts from an assumed compromised process.
Updated•1 month ago
|
Description
•