LSNG `PBackgroundLSSimpleRequest` does not verify the sender principal
Categories
(Core :: Storage: localStorage & sessionStorage, defect)
Tracking
()
People
(Reporter: t.satoki111, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, reporter-external, Whiteboard: [client-bounty-form])
Attachments
(1 file)
|
7.05 KB,
application/x-javascript
|
Details |
Summary
The LSNG simple request path (GetState / IsPreloaded) sends LSSimpleRequestParams over PBackgroundLSSimpleRequest. On the parent side, LSSimpleRequestBase::VerifyRequestParams() calls only VerifyPrincipalInfo() and never calls VerifyClientId(). A compromised content process can therefore forge an LSSimpleRequestGetStateParams message carrying a victim origin's storagePrincipalInfo and receive back the complete live localStorage key/value set for that origin from the global datastore map.
Technical details
Contrast: simple request path vs full request path
| Simple request path | Full request path | |
|---|---|---|
| IPC actor | PBackgroundLSSimpleRequest |
PBackgroundLSRequest |
| Parent base class | LSSimpleRequestBase |
LSRequestBase |
| VerifyRequestParams | VerifyPrincipalInfo() only |
VerifyPrincipalInfo() + VerifyClientId() |
| Sender binding | None | ClientManagerService::HasWindow(ContentParentId, principalInfo, clientId) |
LSSimpleRequestBase::VerifyRequestParams (ActorsParent.cpp:8053)
nsresult LSSimpleRequestBase::VerifyRequestParams() {
MOZ_ASSERT(mParams.type() != LSSimpleRequestParams::T__None);
switch (mParams.type()) {
case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
const LSSimpleRequestPreloadedParams& params =
mParams.get_LSSimpleRequestPreloadedParams();
if (NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(),
params.storagePrincipalInfo(),
false))) {
return NS_ERROR_FAILURE;
}
break;
}
case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: {
const LSSimpleRequestGetStateParams& params =
mParams.get_LSSimpleRequestGetStateParams();
if (NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(),
params.storagePrincipalInfo(),
false))) {
return NS_ERROR_FAILURE;
}
break;
}
// ...
}
return NS_OK;
}
There is no call to VerifyClientId() anywhere in this function.
LSRequestBase::VerifyRequestParams (ActorsParent.cpp:6304) — for comparison
nsresult LSRequestBase::VerifyRequestParams() {
switch (mParams.type()) {
case LSRequestParams::TLSRequestPrepareDatastoreParams: {
const LSRequestPrepareDatastoreParams& params =
mParams.get_LSRequestPrepareDatastoreParams();
if (NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(),
params.storagePrincipalInfo(),
false))) {
return NS_ERROR_FAILURE;
}
if (NS_WARN_IF(!VerifyClientId(mContentParentId,
params.principalInfo(),
params.clientId()))) {
return NS_ERROR_FAILURE;
}
break;
}
// ...
}
return NS_OK;
}
VerifyClientId() cross-checks (ContentParentId, principalInfo, clientId) through ClientManagerService::HasWindow(...) (ActorsParent.cpp:3157), binding the request to a real window that belongs to the sending process. The simple request path has no equivalent check.
How GetStateOp derives the origin and reads the datastore
// GetStateOp::Start / GetResponse (simplified)
mOrigin = quota::GetOriginFromValidatedPrincipalInfo(storagePrincipalInfo);
// ...
RefPtr<Datastore> datastore = GetDatastore(mOrigin);
if (datastore) {
itemInfos = datastore->GetOrderedItems().Clone();
}
gDatastores is a global hashtable keyed by origin string, not by ContentParentId. The lookup uses mOrigin derived directly from the child-supplied storagePrincipalInfo, with no further authorization gate.
VerifyClientId pref gate (ActorsParent.cpp:3157)
VerifyClientId() itself is gated on the gClientValidation pref: when the pref is off, HasWindow() is not called even on the full request path. The simple request path does not call VerifyClientId() at all, so it is unconditionally unverified regardless of the pref.
Abuse scenario
- An attacker compromises a content process through any available vulnerability.
- A victim origin is open in another tab, so its localStorage datastore is live in the parent process.
- The compromised process constructs a
LSSimpleRequestGetStateParamswith bothprincipalInfoandstoragePrincipalInfoset to the victim origin'sContentPrincipalInfo, and sends it overPBackgroundLSSimpleRequest. - The parent passes the self-consistency check in
VerifyPrincipalInfo(), derives the victim origin, and returns the fullitemInfos(all key/value pairs) from the victim datastore. - As a lower-noise precondition check,
LSSimpleRequestPreloadedParamscan be sent first to confirm whether the victim datastore is currently live, providing a cross-origin boolean oracle for datastore presence.
Scope
- Stable desktop Firefox in the default configuration. LSNG is active because
dom.storage.enable_unsupported_legacy_implementationisfalseby default. - Requires a compromised content process or equivalent ability to forge PBackground IPC arguments.
- Cross-origin disclosure of all live localStorage key/value pairs for the victim origin.
- Cross-origin oracle for whether a given origin's datastore is currently active (
IsPreloaded). - Because the parent trusts the full
storagePrincipalInfoincluding its origin attributes, the issue can extend to partitioned storage or private-browsing variants of the victim origin's storage. GetState()only exposes datastores that are currently live in the parent, not all on-disk storage. In practice this is sufficient whenever the victim origin is open or has been preloaded.
Regulation
Logic bug: missing sender principal verification on the simple request path. Not a UAF / buffer overflow / null-deref DoS. Qualifies as a cross-origin information disclosure / privilege escalation after content-process compromise, in scope for Client Bug Bounty.
Recommended fix
- Add a
VerifyClientId()call insideLSSimpleRequestBase::VerifyRequestParams()for each parameter variant, matching whatLSRequestBasealready does on the full request path. - If the simple request IPDL types (
LSSimpleRequestPreloadedParams,LSSimpleRequestGetStateParams) do not carry aclientIdfield today, add one so thatClientManagerService::HasWindow(ContentParentId, principalInfo, clientId)can be called. - Confirm that the
gClientValidationpref gate is always enabled in production builds, or remove the gate entirely.
Updated•8 months ago
|
Updated•8 months ago
|
Updated•8 months ago
|
Comment 1•7 months ago
|
||
Hi,
it looks like this security bug was reported without a test case attachment.
As per our new guidelines, we no longer consider bugs without a test case as elegible for a bug bounty.
For example, a good test case for a memory corruption is an attached HTML file called "testcase.html" that reliably crashes Firefox in an address sanitizer build.
Please re-read our various pages here and submit a reproducible testcase as an attachment to the bug.
- https://www.mozilla.org/en-US/security/bug-bounty/
- https://www.mozilla.org/en-US/security/client-bug-bounty/
- https://www.mozilla.org/en-US/security/bug-bounty/faq/
- https://attackanddefense.dev/2026/03/13/bug-bounty-program-updates-2026.html
(This is an automated comment. Use the filter word "king-taste-soap" if you need to filter bulk email)
| Reporter | ||
Comment 2•7 months ago
|
||
The test mirrors test_clientValidation.js: it enables dom.storage.client_validation
and confirms that xpcshell (which has no associated client window) is rejected.
It then shows that the simple request path (isPreloaded / getState) is NOT rejected
under the same conditions.
How to run:
Place testcase.js in dom/localstorage/test/unit/, add ["testcase.js"] to
xpcshell.toml, then:
./mach xpcshell-test dom/localstorage/test/unit/testcase.js
Result on current trunk:
PASSFull path (PBackgroundLSRequest) is blocked by client validation
FAILisPreloaded (simple path) should enforce the same client validation
as getLocalStorage (full path) - false == true
The full request path correctly throws NS_ERROR_FAILURE because
LSRequestBase::VerifyRequestParams() (ActorsParent.cpp:6304) calls
VerifyClientId(), which calls ClientManagerService::HasWindow() and finds
no matching window.
The simple request path succeeds because
LSSimpleRequestBase::VerifyRequestParams() (ActorsParent.cpp:8059) only calls
VerifyPrincipalInfo() and never calls VerifyClientId(). Additionally,
LSSimpleRequestGetStateParams / LSSimpleRequestPreloadedParams
(PBackgroundLSSharedTypes.ipdlh:65-81) lack clientId and clientPrincipalInfo
fields entirely, so there is nothing to verify even if the call were added.
A compromised content process can exploit this asymmetry to read any origin's
live localStorage via forged PBackgroundLSSimpleRequest messages.
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•5 months ago
|
Description
•