Closed Bug 2022394 Opened 8 months ago Closed 7 months ago

LSNG `PBackgroundLSSimpleRequest` does not verify the sender principal

Categories

(Core :: Storage: localStorage & sessionStorage, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 1853706

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

  1. An attacker compromises a content process through any available vulnerability.
  2. A victim origin is open in another tab, so its localStorage datastore is live in the parent process.
  3. The compromised process constructs a LSSimpleRequestGetStateParams with both principalInfo and storagePrincipalInfo set to the victim origin's ContentPrincipalInfo, and sends it over PBackgroundLSSimpleRequest.
  4. The parent passes the self-consistency check in VerifyPrincipalInfo(), derives the victim origin, and returns the full itemInfos (all key/value pairs) from the victim datastore.
  5. As a lower-noise precondition check, LSSimpleRequestPreloadedParams can 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_implementation is false by 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 storagePrincipalInfo including 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 inside LSSimpleRequestBase::VerifyRequestParams() for each parameter variant, matching what LSRequestBase already does on the full request path.
  • If the simple request IPDL types (LSSimpleRequestPreloadedParams, LSSimpleRequestGetStateParams) do not carry a clientId field today, add one so that ClientManagerService::HasWindow(ContentParentId, principalInfo, clientId) can be called.
  • Confirm that the gClientValidation pref gate is always enabled in production builds, or remove the gate entirely.
Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → Storage: localStorage & sessionStorage
Product: Firefox → Core
Flags: needinfo?(jjalkanen)

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.

(This is an automated comment. Use the filter word "king-taste-soap" if you need to filter bulk email)

Attached file testcase.js —

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.

Status: UNCONFIRMED → RESOLVED
Closed: 7 months ago
Duplicate of bug: pbackground-principal
Resolution: --- → DUPLICATE
Flags: needinfo?(jjalkanen)
Flags: sec-bounty? → sec-bounty-
Group: dom-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: