SandboxBroker one-path OPEN request accepts extra second path and strips nested FORCE_DENY
Categories
(Core :: Security: Process Sandboxing, defect, P2)
Tracking
()
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
- parses the first path, optionally parses a second path, looks up both paths, and intersects
security/sandbox/linux/broker/SandboxBroker.cpp:847-853- checks
FORCE_DENYonly after that intersection
- checks
security/sandbox/linux/broker/SandboxBroker.cpp:860-871SANDBOX_FILE_OPENcallsopen(pathBuf, ...)using only the first path
security/sandbox/linux/SandboxBrokerClient.cpp:163-177- normal
Open()sendsnullptras the second path, so the PoC uses the raw broker socket primitive available to a compromised content process
- normal
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_whitelistandsecurity.sandbox.content.read_path_whitelistare empty by default
security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp:554-573- when
XDG_CONFIG_HOMEis not set,$HOME/.config/is recursively allowed read access
- when
security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp:579-607$HOME/.config/mozilla/is explicitly added asFORCE_DENY
xpcom/io/nsAppFileLocationProvider.cpp:169-170- UNIX profile data uses
~/.mozilla/or${XDG_CONFIG_HOME:-~/.config}/mozilla
- UNIX profile data uses
toolkit/tests/gtest/TestXREAppDir.cpp:618-630and:667-681- existing tests assert
$HOME/.config/mozilla/firefoxis used when$HOME/.mozilladoes not exist andXDG_CONFIG_HOMEis not set
- existing tests assert
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:
- Builds a synthetic broker policy with recursive
MAY_READon an outer directory and nestedFORCE_DENYon a child directory. - Verifies normal raw broker
SANDBOX_FILE_OPENof the denied first path alone returns-EACCES. - Sends a raw
SANDBOX_FILE_OPENrequest containing a denied first path followed by an allowed second path. - Verifies the broker returns a readable fd for the denied first path.
- 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.
- Repeats the proof with the exact default policy shape from
SandboxBrokerPolicyFactory.cpp: recursive read on.config/plusFORCE_DENYon.config/mozilla/. - 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
-EACCESforO_RDWRandO_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.
| Reporter | ||
Comment 1•5 months ago
|
||
Updated•5 months ago
|
Updated•5 months ago
|
| Reporter | ||
Updated•5 months ago
|
| Assignee | ||
Comment 2•5 months ago
|
||
Looks like the same impact / exploitability as bug 2034713.
| Assignee | ||
Comment 3•5 months ago
|
||
Comment 4•5 months ago
|
||
The severity field is not set for this bug.
:jstutte, could you have a look please?
For more information, please visit BugBot documentation.
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Comment 5•5 months ago
|
||
The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.
Comment 7•5 months ago
|
||
https://hg-edge.mozilla.org/mozilla-central/rev/922012531c45
Please request Beta and ESR140 uplift on this.
| Assignee | ||
Comment 8•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D302075
Updated•5 months ago
|
Comment 9•5 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Bug 2040515 / D301364: sec-high
Bug 2040177 / D302075: sec-moderate (similar to bug 2034713)
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
Updated•5 months ago
|
Updated•5 months ago
|
Comment 10•5 months ago
|
||
| uplift | ||
| Assignee | ||
Comment 11•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D302075
Updated•5 months ago
|
Comment 12•5 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Bug 2040515 / D301463: sec-high
Bug 2040177 / D302075: sec-moderate (similar to bug 2034713)
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
Comment 13•5 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Bug 2040515 / D301463: sec-high
Bug 2040177 / D302075: sec-moderate (similar to bug 2034713)
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
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Comment 14•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•1 month ago
|
Description
•