Closed Bug 2049148 (CVE-2026-74960) Opened 4 months ago Closed 3 months ago

Cross-extension runtime.Port hijack via Conduits cast

Categories

(WebExtensions :: General, defect, P2)

defect

Tracking

(firefox-esr115 wontfix, firefox-esr140154+ fixed, firefox-esr153154+ fixed, firefox152 wontfix, firefox153 wontfix, firefox154+ fixed)

RESOLVED FIXED
154 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 154+ fixed
firefox-esr153 154+ fixed
firefox152 --- wontfix
firefox153 --- wontfix
firefox154 + fixed

People

(Reporter: vanirxxx, Assigned: zombie)

References

Details

(4 keywords, Whiteboard: [client-bounty-form][addons-jira][adv-main154+][adv-esr153.1+][adv-esr140.14+])

Attachments

(1 file, 4 external files, 1 obsolete file)

3.68 KB, application/x-zip-compressed
Details
Attached file poc.zip —

Summary

A compromised content process can inject into and eavesdrop on another extension's privileged runtime.Port. The parent-side WebExtension message router ConduitsParent._cast (kind "port") selects delivery targets by portId alone, with none of the sender/extension verification its sibling "messenger" filter performs.

Affected

  • Firefox Nightly 153.0a1, BuildID 20260612072730, changeset c8c933829aec.
  • OS: All (logic bug, platform-independent; reproduced on Linux).
  • Component: WebExtensions :: General (the Conduits IPC router).

Defect 1 — the "port" cast filter has no sender/extension verification

toolkit/components/extensions/ConduitsParent.sys.mjs, _cast():

let filters = {
  // VULNERABLE: targets any endpoint whose portId matches — no verified, no extensionId.
  port: remote =>
    remote.portId === arg.portId &&
    (arg.source == null || remote.source === arg.source),

  // SIBLING (safe): messenger requires verified + same extension.
  messenger: r =>
    r.verified &&
    r.id !== arg.sender.contextId &&
    r.extensionId === arg.extensionId &&
    r.recv.includes(method) && ...,
};
let targets = Array.from(Hub.remotes.values()).filter(filters[kind]);
let promises = targets.map(c => this._send(method, !!arg.query, c.id, arg));

A PortMessage from a compromised content process is therefore delivered to any port endpoint with a matching portId, regardless of which extension/principal owns it.

Defect 2 — the child-supplied portId is never validated at conduit open

ConduitsParent.sys.mjs recvConduitOpened → fillInAddress → verifyEnv: verifyEnv validates extensionId/principal/process, but never portId/source. So a compromised content process opens a conduit asserting an arbitrary (victim) portId.

Defect 3 — the portId secret leaks to content and is low-entropy

  • getUniqueId() = (uniqueProcessID & 0xffff) * 2**37 + (nextId++) (ExtensionUtils.sys.mjs): both factors are small dense counters, not nonces → live-portId candidate space ≈ low thousands, with a match-oracle (the silent cast routing + recvPortMessage/PortDisconnect feedback).
  • Disclosure: getSender stamps sender.contextId = source.id (an extension-process getUniqueId) and ships it to content scripts via the unverified "tab" cast (tab filter has no verified check); tabs.connect/sendMessage from a privileged background casts the actual portId to the content script (recvPortConnect). Same per-process counter feeds contextId and portId (cross-id reuse).

End-to-end chain

  1. Compromised content process (extension attacker@poc) learns a victim contextId/portId from a delivered sender/PortConnect (Defect 3), or brute-forces the low-entropy space with the routing oracle.
  2. It opens a conduit asserting the victim portId (Defect 2, no validation), and sends PortMessage{portId: victim, source} — ProxyMessenger.recvPortMessage → castPortMessage("port", {portId, source}).
  3. _cast "port" filter (Defect 1) matches the victim's real port endpoint by portId and delivers → the victim extension's privileged runtime.Port.onMessage fires with attacker data; symmetrically the attacker registers on the channel and eavesdrops on the victim's messages.
  4. Impact = content→privileged-extension message injection/eavesdrop: the background acts on messages it believes came from its own content script (privileged-API actions on the content script's behalf), and the attacker reads data the extension carries over the port. UXSS-class against the extension's principal.

PoC reproduction

poc/poc_conduit.js drives the real ConduitsParent module under xpcshell: registers a victim privileged port endpoint + a bystander in Hub.remotes, creates an attacker BroadcastConduit, and calls the real castPortMessage("port", {portId: victim}). Run: obj-asan-fuzz/dist/bin/xpcshell -g obj-asan-fuzz/dist/bin -a obj-asan-fuzz/dist/bin poc_conduit.js. See poc/RESULTS.md for the A/B output.

Empirical proof

Routing bug REPRODUCED on c8c933829aec (real module under xpcshell):

RUN 1 (as shipped):  -> DELIVERED to [VICTIM privileged ext port]   INJECTION SUCCEEDED
                     messenger sibling, same cross-extension cast:  BLOCKED
RUN 2 (one-line fix: + remote.verified && remote.extensionId === arg.extensionId):  injection BLOCKED

This confirms Defect 1 end-to-end at the routing layer and that the proposed fix closes it. The portId was supplied directly (standing in for Defect 3's separately-argued leak/brute-force, which is source-traced, not executed). No ASAN signature — auth/logic bug.

Proposed fix

Bring the "port" filter to parity with "messenger" — require the target endpoint be verified and belong to the sender's extension, and have the port cast carry the sender's extensionId:

port: remote =>
  remote.portId === arg.portId &&
  (arg.source == null || remote.source === arg.source) &&
  remote.verified && remote.extensionId === arg.extensionId,

Defense-in-depth: validate/scope portId at recvConduitOpened (bind a port to the channel participants established at PortConnect), and stop shipping raw contextId/portId to content where not needed.

Flags: sec-bounty?
Component: Security → General
Product: Firefox → WebExtensions

The non-secrecy of contextId is known (bug 1628178).

We should do a better job at strictly verifying whether IPC from/to a specific target is really allowed.
Although ports can originate from any process, once created it is pretty clear where the 1-n endpoints are.

Keywords: ai-involved
See Also: → 1628178

Isn't a content script executed in the content process anyway and it's expected that a compromised content process can send messages that seem to come from the content script?

Flags: needinfo?(rob)

The rating assigned by our bug rating tool (currently under development) is:

Rating: sec-moderate

Explanation: Cross-extension runtime.Port hijack via Conduits cast missing extensionId/verified checks. The vulnerability is real and confirmed by code inspection. The _cast() port filter in ConduitsParent.sys.mjs only checks portId and optionally source, lacking the verified and extensionId checks that the messenger filter enforces. Combined with the fact that portId is not validated at conduit registration (recvConduitOpened/verifyEnv only checks extensionId and process, not portId) and that getUniqueId() produces low-entropy IDs, an attacker who registers a conduit asserting a victim's portId can inject messages into another extension's privileged background port. However, exploiting this requires a compromised content process (native code execution in the renderer) to craft IPC messages with arbitrary portIds — normal JavaScript (even from a malicious extension's content script) cannot directly control the portId assigned by the extension API layer (getUniqueId() is called internally). This is not reachable from normal browsing. The impact, while real — allowing cross-extension port message injection that could cause a victim extension to perform privileged actions based on attacker-controlled data — depends on what the victim extension does with port messages. The 'UXSS-class' characterization is overstated. The bug represents a meaningful IPC privilege escalation path from a compromised content process to another extension's background, which fits sec-moderate (a sec-high-class issue requiring an unusual prerequisite).

Please ni? me if you disagree with this rating.

Apparently, this is accessing ports which belong to other content processes.

Flags: needinfo?(rob)
Severity: -- → S3
Status: UNCONFIRMED → NEW
Ever confirmed: true
Priority: -- → P2
Whiteboard: [client-bounty-form] → [client-bounty-form][addons-jira]
Duplicate of this bug: 2051471
Attached file (secure) —
Assignee: nobody → tomica
Status: NEW → ASSIGNED
Attached file (secure) —
Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Flags: in-testsuite+
Resolution: --- → FIXED
Target Milestone: --- → 154 Branch
Duplicate of this bug: 2055100

Please add uplift requests for ESR140 and ESR153

Flags: needinfo?(tomica)

firefox-esr140 Uplift Approval Request

  • User impact if declined/Reason for urgency: A compromised process might be able to access messages from/to other extensions.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: medium
  • Explanation of risk level: The fix is somewhat limited in scope, but not trivial. Worse case would be the fix disables some legitimate extensions from working, but it should be fairly obvious from the error logs if that happens.
  • String changes made/needed?: Log only (new error logs added, which we don't localize).
  • Is Android affected?: yes
Attachment #9613667 - Flags: approval-mozilla-esr140?

firefox-esr153 Uplift Approval Request

  • User impact if declined/Reason for urgency: A compromised process might be able to access messages from/to other extensions.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: medium
  • Explanation of risk level: The fix is somewhat limited in scope, but not trivial. Worse case would be the fix disables some legitimate extensions from working, but it should be fairly obvious from the error logs if that happens.
  • String changes made/needed?: Log only (new error logs added, which we don't localize).
  • Is Android affected?: yes
Attachment #9613668 - Flags: approval-mozilla-esr153?

firefox-release Uplift Approval Request

  • User impact if declined/Reason for urgency: A compromised process might be able to access messages from/to other extensions.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: medium
  • Explanation of risk level: The fix is somewhat limited in scope, but not trivial. Worse case would be the fix disables some legitimate extensions from working, but it should be fairly obvious from the error logs if that happens.
  • String changes made/needed?: Log only (new error logs added, which we don't localize).
  • Is Android affected?: yes
Attachment #9613669 - Flags: approval-mozilla-release?
Attached file (secure) (obsolete) —
Attachment #9613669 - Attachment is obsolete: true
Attachment #9613669 - Flags: approval-mozilla-release?
Attachment #9613667 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9613668 - Flags: approval-mozilla-esr153? → approval-mozilla-esr153+
Flags: needinfo?(tomica)
QA Whiteboard: [sec] [qa-triage-done-c155/b154]
Whiteboard: [client-bounty-form][addons-jira] → [client-bounty-form][addons-jira][adv-main154+][adv-esr153.1+][adv-esr140.14+]
Alias: CVE-2026-74960
Flags: sec-bounty? → sec-bounty-
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: