Closed Bug 2017857 (CVE-2026-6761) Opened 8 months ago Closed 7 months ago

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)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 150+ fixed
firefox148 --- wontfix
firefox149 --- wontfix
firefox150 + fixed
firefox151 + fixed

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)

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:

  1. Bind TCP server sockets on 0.0.0.0 (all interfaces) — accessible from the local network, not just localhost. The loopbackOnly parameter is hardcoded to false at dom/network/TCPServerSocket.cpp:74.
  2. Create unfiltered UDP sockets — the principal parameter is explicitly /* unused */ and an empty filter string bypasses the STUN packet filter entirely.
  3. 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.h does NOT grant network-bind or network-inbound
  • Linux: seccomp filter denies SYS_SOCKET for 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

  1. Add process-type validation to both allocators (matching RecvGetPageThumbStream pattern):
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
}
  1. Implement principal validation for UDP — the code explicitly acknowledges it's missing.

  2. Enforce packet filter for UDP — reject empty filter strings from non-privileged processes, or require the "stun" filter for WebRTC-only usage.

  3. Consider removing the IPDL messages: TCPServerSocket was 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): checks PRIVILEGEDABOUT_REMOTE_TYPE
  • RecvCreateWindow (ContentParent.cpp:5403): calls ValidatePrincipal
Flags: sec-bounty?
Group: firefox-core-security → core-security
Component: Security → Networking
Keywords: ai-involved
Product: Firefox → Core

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.

Group: core-security → network-core-security
Severity: -- → S3
Priority: -- → P2
Whiteboard: [client-bounty-form] → [client-bounty-form][necko-triaged][necko-priority-new]

I don't think this is a sandbox escape as suggested by the original report. It is a privilege escalation attack though.

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 :)

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.

Duplicate of this bug: 2023105
Attached file (secure) —
Assignee: nobody → valentin.gosu
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Duplicate of this bug: 2023420
Pushed by sstanca@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/0456a1508e2a https://hg.mozilla.org/integration/autoland/rev/9d06cd6608c6 Revert "Bug 2017857 - Enforce prefs for dom TCP and UDP sockets r=necko-reviewers,jesup,tschuster" for causing multiple failures.

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)

Flags: needinfo?(valentin.gosu)
Flags: needinfo?(valentin.gosu)
Pushed by asilaghi@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/20f70df66e4c https://hg.mozilla.org/integration/autoland/rev/ea6bafa69d72 Revert "Bug 2017857 - Enforce prefs for dom TCP and UDP sockets r=necko-reviewers,jesup,tschuster" for causing multiple failures at RecvPUDPSocketConstructor
Hit MOZ_CRASH(Use IPC_FAIL only in an unrecoverable, unexpected state: RecvPUDPSocketConstructor udp socket not enabled 
Flags: needinfo?(valentin.gosu)

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.

Flags: needinfo?(ghess)
Flags: needinfo?(valentin.gosu)
Flags: needinfo?(ghess)
Group: network-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

The patch landed in nightly and beta is affected.
:valentin, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(valentin.gosu)
See Also: → 2026816

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?

AFAIK we use the socket process on desktop, but main process on Android?

Flags: needinfo?(valentin.gosu)

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
Attachment #9559227 - Flags: approval-mozilla-beta?
Attached file (secure) —
Attachment #9559227 - Flags: approval-mozilla-beta? → approval-mozilla-beta+

Please also request uplift to ESR140.

Flags: needinfo?(valentin.gosu)
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]
Flags: needinfo?(valentin.gosu)
Attached file (secure) —
Attachment #9560452 - Flags: approval-mozilla-esr140?

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
Attachment #9560452 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Flags: sec-bounty? → sec-bounty+
Whiteboard: [client-bounty-form][necko-triaged][necko-priority-new] → [client-bounty-form][necko-triaged][necko-priority-new][adv-main150+]
Whiteboard: [client-bounty-form][necko-triaged][necko-priority-new][adv-main150+] → [client-bounty-form][necko-triaged][necko-priority-new][adv-main150+][adv-esr140.10+]
Alias: CVE-2026-6761
Group: core-security-release
Duplicate of this bug: 2084447
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: