Closed Bug 2040177 (CVE-2026-12311) Opened 5 months ago Closed 5 months ago

SandboxBroker one-path OPEN request accepts extra second path and strips nested FORCE_DENY

Categories

(Core :: Security: Process Sandboxing, defect, P2)

defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 152+ fixed
firefox151 --- wontfix
firefox152 + fixed
firefox153 + fixed

People

(Reporter: iifizzyx, Assigned: jld)

References

Details

(5 keywords, Whiteboard: [adv-main152+][adv-esr140.12+])

Attachments

(2 files, 3 external files)

Steps to reproduce:

Checked out current mozilla-central / Nightly source and built with tests enabled.

Source tested:

Source HEAD: 18e5fb40d97a66af059f724dae98a894804c9077
Commit date: 2026-05-16T03:50:02+00:00
Firefox version: Mozilla Firefox 152.0a1
BuildID: 20260516231722

Reviewed the Linux sandbox broker receive path:

  • security/sandbox/linux/broker/SandboxBroker.cpp:783-841
    • parses the first path, optionally parses a second path, looks up both paths, and intersects perms &= perms2
  • security/sandbox/linux/broker/SandboxBroker.cpp:847-853
    • checks FORCE_DENY only after that intersection
  • security/sandbox/linux/broker/SandboxBroker.cpp:860-871
    • SANDBOX_FILE_OPEN calls open(pathBuf, ...) using only the first path
  • security/sandbox/linux/SandboxBrokerClient.cpp:163-177
    • normal Open() sends nullptr as the second path, so the PoC uses the raw broker socket primitive available to a compromised content process

Reviewed the default Linux policy/profile shape:

  • browser/app/profile/firefox.js:1600-1619
    • default Linux content sandbox level is 6
    • security.sandbox.content.write_path_whitelist and security.sandbox.content.read_path_whitelist are empty by default
  • security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp:554-573
    • when XDG_CONFIG_HOME is not set, $HOME/.config/ is recursively allowed read access
  • security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp:579-607
    • $HOME/.config/mozilla/ is explicitly added as FORCE_DENY
  • xpcom/io/nsAppFileLocationProvider.cpp:169-170
    • UNIX profile data uses ~/.mozilla/ or ${XDG_CONFIG_HOME:-~/.config}/mozilla
  • toolkit/tests/gtest/TestXREAppDir.cpp:618-630 and :667-681
    • existing tests assert $HOME/.config/mozilla/firefox is used when $HOME/.mozilla does not exist and XDG_CONFIG_HOME is not set

I also ran the existing profile-directory gtests:

./mach gtest 'NonExistentLegacyXREAppDir_NoEnv.GetUserAppDataDirectory:XDGXREAppDir_NoEnv.GetUserAppDataDirectory'

Both passed, confirming the fresh-profile .config/mozilla/firefox path behavior in the current tree.

Wrote security/sandbox/linux/gtest/TestBroker_ExtraPathDenyBypass.cpp and registered it in security/sandbox/linux/gtest/moz.build by adding "TestBroker_ExtraPathDenyBypass.cpp" to UNIFIED_SOURCES.

The attached gtest does the following:

  1. Builds a synthetic broker policy with recursive MAY_READ on an outer directory and nested FORCE_DENY on a child directory.
  2. Verifies normal raw broker SANDBOX_FILE_OPEN of the denied first path alone returns -EACCES.
  3. Sends a raw SANDBOX_FILE_OPEN request containing a denied first path followed by an allowed second path.
  4. Verifies the broker returns a readable fd for the denied first path.
  5. Confirms the allowed second path does not need to exist; the broker only uses it for policy lookup and still opens the denied first path.
  6. Repeats the proof with the exact default policy shape from SandboxBrokerPolicyFactory.cpp: recursive read on .config/ plus FORCE_DENY on .config/mozilla/.
  7. Repeats the proof through the real SandboxBrokerPolicyFactory::GetContentPolicy() with the default Linux Firefox prefs:
security.sandbox.content.level = 6
security.sandbox.content.write_path_whitelist = ""
security.sandbox.content.read_path_whitelist = ""

The gtest harness does not load browser/app/profile/firefox.js, so the test sets those default values explicitly before asking the policy factory for the content policy.

Two negative controls are included:

  • denied first path plus writable/create second path still returns -EACCES for O_RDWR and O_RDONLY | O_TRUNC
  • normal brokered symlink() cannot plant a symlink to the denied profile target under a writable directory

Ran:

./mach gtest 'SandboxBrokerExtraPathDenyBypass*'

Result:

TEST_START: SandboxBrokerExtraPathDenyBypass.OpenIgnoresUnexpectedSecondPath
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.SecondPathNeedNotExist
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.CurrentDefaultContentPolicyShape
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.RealWorldMozillaConfigDenyShape
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.ExtraPathDoesNotCreateWriteAccess
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.NormalOpenFollowsAllowedSymlinkToDeniedTarget
TEST_END: PASS
TEST_START: SandboxBrokerExtraPathDenyBypass.NormalSymlinkCannotPlantDeniedTargetUnderWritableDir
TEST_END: PASS
Ran 7 checks (7 tests)
Unexpected results: 0
OK

Attacker primitive from a compromised content process:

SandboxBrokerCommon::Request{
    SandboxBrokerCommon::Operation::SANDBOX_FILE_OPEN,
    O_RDONLY,
    0,
    0,
}

followed by two NUL-terminated path strings:

first path  = denied target path
second path = allowed path under the same recursively readable ancestor

The normal SandboxBrokerClient::Open() wrapper does not send a second path. This is a raw broker protocol issue reachable from a compromised content process that can write to its broker fd.

Actual results:

SandboxBroker::ThreadMain accepts the unexpected second path for SANDBOX_FILE_OPEN.

The first path is looked up as denied:

MAY_ACCESS | MAY_READ | FORCE_DENY = 0x83

The second path is looked up as allowed:

MAY_ACCESS | MAY_READ = 0x3

The broker then intersects the two masks:

perms &= perms2;

That clears FORCE_DENY while preserving MAY_ACCESS | MAY_READ.

The deny check runs after the intersection:

} else if ((perms & FORCE_DENY) == FORCE_DENY) {
  resp.mError = -EACCES;
} else if (permissive || perms & MAY_ACCESS) {

Because FORCE_DENY has already been stripped, SANDBOX_FILE_OPEN proceeds and opens pathBuf, the first path:

openedFd = open(pathBuf, req.mFlags | kRequiredOpenFlags, 0600);

The second path influenced authorization but is not the file opened.

Observable post-conditions from the PoC:

Direct denied raw OPEN:
  first path = denied file
  second path = none
  result = -EACCES

Malformed raw OPEN:
  first path = denied file
  second path = allowed sibling path
  result = readable fd for the denied first file

The allowed second path does not need to exist. It only has to match an allow rule because the broker does not open it for SANDBOX_FILE_OPEN.

Impact:

This is a Linux content sandbox broker policy bypass. From a compromised content process, an attacker can bypass a nested FORCE_DENY rule and read a first path that the broker denies when requested normally.

The demonstrated default-policy shape is the same one in SandboxBrokerPolicyFactory.cpp, where .config/ is recursively readable and .config/mozilla/ is explicitly denied. In current Linux profile-directory logic, a fresh profile without an existing legacy $HOME/.mozilla directory and without XDG_CONFIG_HOME uses $HOME/.config/mozilla/firefox.

That places default fresh-profile files under the exact denied subtree. The bypass can expose restricted profile files such as:

$HOME/.config/mozilla/firefox/<profile>/cookies.sqlite
$HOME/.config/mozilla/firefox/<profile>/logins.json
$HOME/.config/mozilla/firefox/<profile>/key4.db
$HOME/.config/mozilla/firefox/<profile>/cert9.db
$HOME/.config/mozilla/firefox/<profile>/places.sqlite

This is cross-origin cookie, saved-login material, key database, certificate database, and browsing-history disclosure from a compromised content process on affected default fresh-profile Linux layouts.

This does not by itself demonstrate arbitrary native code execution outside the sandbox. The confirmed primitive is restricted file read through the sandbox broker. I also tested the likely write-upgrade path (O_RDWR and O_TRUNC with a writable second path), and it remains denied because the first path does not carry MAY_WRITE or MAY_CREATE.

Expected results:

For one-path operations, the broker should reject a request payload containing an extra pathname after the first NUL.

For two-path operations, deny semantics should be sticky: if either path has FORCE_DENY, the final authorization state should preserve the deny rather than allowing a plain bitwise intersection to clear it.

Possible fix direction:

const bool opTakesSecondPath =
    req.mOp == SANDBOX_FILE_LINK ||
    req.mOp == SANDBOX_FILE_SYMLINK ||
    req.mOp == SANDBOX_FILE_RENAME;

if (pathLen2 > 0 && !opTakesSecondPath) {
  perms = 0;
}

For actual two-path operations, preserve FORCE_DENY explicitly before intersecting ordinary allow bits.

Attached file gtest_output.log —
Group: core-security → dom-core-security
Flags: sec-bounty?

Looks like the same impact / exploitability as bug 2034713.

Assignee: nobody → jld

The severity field is not set for this bug.
:jstutte, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(jstutte)
Severity: -- → S2
Flags: needinfo?(jstutte)
Priority: -- → P2

The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.

Status: UNCONFIRMED → NEW
Ever confirmed: true

https://hg-edge.mozilla.org/mozilla-central/rev/922012531c45

Please request Beta and ESR140 uplift on this.

Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Flags: needinfo?(jld)
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
Attachment #9593356 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

Notes: The second patch depends on the first. esr140 will be requested separately because it needs a merge fix for 2040515.

  • 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: medium
  • Explanation of risk level: Bug 2040515 / D301364: medium; this is entirely removing support for brokered symlink() calls, but the current policies allow it in a very limited set of cases and as far as I know nothing is using it (the places where file creation is allowed should be for regular files only). I can see an argument for calling this patch low risk, but I'm not quite comfortable with that.

Bug 2040177 / D302075: low; this is checking for invalid data that can't be sent normally, and fixing a permissions check issue that wouldn't have affected any legitimate use (I think the second half is unobservable without exploiting the first half, given the current sandbox policies).

  • String changes made/needed?: none
  • Is Android affected?: no
Attachment #9593356 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9593370 - Flags: approval-mozilla-esr140?

firefox-beta Uplift Approval Request

Notes: The second patch depends on the first. esr140 will be requested separately because it needs a merge fix for 2040515.

  • 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: medium
  • Explanation of risk level: Bug 2040515 / D301463: medium; this is entirely removing support for brokered symlink() calls, but the current policies allow it in a very limited set of cases and as far as I know nothing is using it (the places where file creation is allowed should be for regular files only). I can see an argument for calling this patch low risk, but I'm not quite comfortable with that.

Bug 2040177 / D302075: low; this is checking for invalid data that can't be sent normally, and fixing a permissions check issue that wouldn't have affected any legitimate use (I think the second half is unobservable without exploiting the first half, given the current sandbox policies).

  • String changes made/needed?: none
  • Is Android affected?: no

firefox-esr140 Uplift Approval Request

Note: The second patch depends on the first.

  • 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: medium
  • Explanation of risk level: Bug 2040515 / D301463: medium; this is entirely removing support for brokered symlink() calls, but the current policies allow it in a very limited set of cases and as far as I know nothing is using it (the places where file creation is allowed should be for regular files only). I also checked that there were no relevant policy changes between esr140 and main that would affect this assessment.

Bug 2040177 / D302075: low; this is checking for invalid data that can't be sent normally, and fixing a permissions check issue that wouldn't have affected any legitimate use (I think the second half is unobservable without exploiting the first half, given the current sandbox policies).

  • String changes made/needed?: none
  • Is Android affected?: no
Attachment #9593370 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Flags: needinfo?(jld)
QA Whiteboard: [sec] [uplift] [qa-triage-done-c153/b152]
Whiteboard: [adv-main152+]
Whiteboard: [adv-main152+] → [adv-main152+][adv-esr140.12+]
Alias: CVE-2026-12311
Flags: sec-bounty? → sec-bounty+
See Also: → CVE-2026-8958
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: