Closed Bug 2044536 (CVE-2026-16391) Opened 4 months ago Closed 3 months ago

PBM IndexedDB encrypted stream exposes unzeroed pre-encryption buffer tail bytes to the content process through EncryptedFileInputStreamParams

Categories

(Core :: Storage: IndexedDB, defect, P3)

defect

Tracking

()

RESOLVED FIXED
154 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 153+ fixed
firefox152 --- wontfix
firefox153 + fixed
firefox154 + fixed

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
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
Attached file repro.patch —

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:

  1. A content-process probe in dom/quota/DecryptingInputStream_impl.h at DecryptingInputStream::Deserialize(). The probe runs in the patched content process and uses only the received EncryptedFileInputStreamParams key and the deserialized raw base stream to decrypt the final encrypted block.
  2. A proof seed in dom/quota/EncryptingOutputStream_impl.h. It fills the newly allocated plaintext buffer with 0xa0 + (offset & 0x0f) before the logical write. A one-byte Blob overwrites only mBuffer[0], so mBuffer[1..15] remains as known pre-encryption buffer content.
  3. A browser-chrome test dom/indexedDB/test/browser_encrypted_tail_probe.js that opens a private window, stores a one-byte Blob in IndexedDB, then reads the Blob back to force the parent-to-child RemoteLazyInputStream path.

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:

EncryptedBlock.h:69

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;
Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → Storage: IndexedDB
Product: Firefox → Core
Severity: -- → S3
Priority: -- → P3
Assignee: nobody → abienner
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Attached file (secure) —
Pushed by abienner@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/de2f0aa3b7c9 https://hg.mozilla.org/integration/autoland/rev/488aa43d7c59 Quota: make EncryptedBlock::RoundedUpToBasicBlockSize public and use it in EncryptingOutputStream, and propertly clear buffer r=dom-storage-reviewers,jari
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 154 Branch
Attached file (secure) —
Attachment #9603746 - Flags: approval-mozilla-beta?

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
Attachment #9603746 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attached file (secure) —
Attachment #9603969 - Flags: approval-mozilla-esr140?

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
Attachment #9603969 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c154/b153]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main153+][adv-ESR140.13+]
Alias: CVE-2026-16391

as presented, this is not impactful enough to qualify for a bounty that starts from an assumed compromised process.

Flags: sec-bounty?
Flags: sec-bounty-hof+
Flags: sec-bounty-
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: