Closed Bug 2033275 (CVE-2026-8972) Opened 5 months ago Closed 5 months ago

CamerasParent::RecvAllocateCapture bypasses permission via IsWindowCapturing

Categories

(Core :: WebRTC: Audio/Video, defect)

defect

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox150 --- wontfix
firefox151 + fixed
firefox152 + fixed

People

(Reporter: pakhunov.anton.n, Assigned: mjf, NeedInfo)

References

(Regression)

Details

(6 keywords, Whiteboard: [adv-main151+])

Attachments

(5 files)

While looking at the PCameras IPC handler for camera allocation, I found that RecvAllocateCapture in dom/media/systemservices/CamerasParent.cpp (~line 1215) accepts a child-supplied uint64_t aWindowID without checking that the window belongs to the sending content process.

The permission check has three branches:

bool allowed = HasCameraPermission(aWindowID);
if (!allowed && Preferences::GetBool("media.navigator.permission.disabled", false)) {
    allowed = true;
}
if (!allowed && IsWindowCapturing(aWindowID, unique_id)) {
    allowed = true;  // third branch - no ownership check
}

HasCameraPermission consumes a one-shot token tied to a real getUserMedia grant. The third branch, IsWindowCapturing, walks sAggregators - a process-global StaticRefPtr shared across all CamerasParent instances (assigned via mAggregators(sAggregators) in the constructor). It returns true if any window in the browser is currently streaming that device, regardless of which content process owns it.

The attack: victim tab (example.com) calls getUserMedia({video:true}). This populates sAggregators[(victimWindowId, deviceUniqueId)] and exhausts the HasCameraPermission token. A compromised renderer for attacker tab (example.net, different content process) then calls AllocateCapture(CameraEngine, deviceUniqueId, victimWindowId) directly over the PCameras channel. HasCameraPermission(victimWindowId) returns false - token gone. IsWindowCapturing(victimWindowId, deviceUniqueId) returns true - victim is streaming. allowed flips to true and AllocateCapture returns a valid streamId.

From a working mochitest - the attacker own getUserMedia failed with NotReadableError (no permission), but the direct IPC path succeeded and delivered frames:

[CameraPoC] EXPLOIT SUCCESS: AllocateCapture returned streamId=1 for VICTIM windowId=17179869185
[CameraPoC] EXPLOIT SUCCESS: DeliverFrame frame=1 size=640x480 streamId=1
... 30+ frames delivered ...
attacker getUserMedia: {"ok":false,"err":"NotReadableError: Failed to allocate videosource"}

A parent-process sentinel written inside the bypass branch confirms the path:

bypass=1 windowId=17179869185 device=3F45E80A-0176-46F7-B185-BB9E2C0E82E3

The fix is to verify that aWindowID belongs to the sending process before the IsWindowCapturing branch can apply - something like checking WindowGlobalParent::GetByInnerWindowId(aWindowID) and confirming the resolved window content parent matches the actor OtherPid(). Without that, any renderer that can enumerate a victim inner window ID can piggyback on an active camera stream.

Attached file PoC: CamerasChildPoC.h —

The missing ownership check extends to all handlers that call GetAggregator(). The root issue is that GetAggregator() searches the global sAggregators without verifying the located stream was allocated by the requesting CamerasParent instance.

Affected handlers beyond RecvAllocateCapture:

RecvStopCapture (around line 1539) - attacker sends a foreign streamId to stop another process's active camera stream. GetAggregator returns the victim's stream, StopStream() sets mActive=false and calls UpdateDevice(). The victim's camera goes dark.

RecvStartCapture (around line 1342) - attacker sends a foreign streamId to reconfigure another process's stream (change resolution, FPS, trigger UpdateDevice()), or register itself as a frame recipient.

RecvReleaseCapture (around line 1304) - attacker destroys another process's stream entirely. If it was the last stream on an aggregator, the capturer is torn down. The victim's subsequent IPC calls will fail.

RecvFocusOnSelectedSource (around line 1410) - attacker triggers FocusOnSelectedSource on the victim's active capturer.

The fix needs to add an ownership check at the GetAggregator level, not just in RecvAllocateCapture. The stream struct has a mParent pointer (CamerasParent*) - the correct check is to verify stream->mParent == this before returning the aggregator to the caller. Without this, a compromised renderer that can guess or brute-force a small sequential streamId can manipulate any active camera stream in the browser.

Group: core-security → media-core-security
Component: WebRTC → WebRTC: Audio/Video
Assignee: nobody → mfroman
Severity: -- → S2
Attached file (secure) —

While the potential outcome is not great, is there much actual risk of an attack? How would the attacker know the victim was granting camera access at that moment, and then having guessed a valid WindowID, try to steal the stream before we give it to the legitimate context that asked for it?

Keywords: sec-low
Status: UNCONFIRMED → NEW
Ever confirmed: true

(In reply to Daniel Veditz [:dveditz] from comment #6)

try to steal the stream before we give it to the legitimate context that asked for it?

Just to be clear here, there is no race one has to fit the attack within. Once permission is given for one window to use a device, an attacker could tag along and subscribe to the frames from that device. The rest of your comment stands, though.

Duplicate of this bug: 2035115

Andreas, it looks like the line checking if (!allowed && IsWindowCapturing(aWindowID, unique_id)) was added in Bug 1771789. Does that seem correct? I'm trying build the info I need to request the sec approval.

Flags: needinfo?(apehrson)

Adding leave-open so I remember to come back to check on how reasonable it would be to add a test.

Keywords: leave-open

(In reply to Michael Froman [:mjf] from comment #9)

Andreas, it looks like the line checking if (!allowed && IsWindowCapturing(aWindowID, unique_id)) was added in Bug 1771789. Does that seem correct? I'm trying build the info I need to request the sec approval.

Yes, it was to accomodate MediaStreamTrack.clone() on a live video capture track when the permission allowing getUserMedia() to resolve without a prompt had been pulled.

Flags: needinfo?(apehrson)
Duplicate of this bug: 2035365

Comment on attachment 9572508 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Possible, but requires an already compromised content process to utilize.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: release
  • If not all supported branches, which bug introduced the flaw?: Bug 1771789
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: not difficult, low risk
  • How likely is this patch to cause regressions; how much testing does it need?: low
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Unknown
Attachment #9572508 - Flags: sec-approval?

Comment on attachment 9572508 [details]
(secure)

sec-low bugs don't need sec-approval to land. Go ahead and push.

Flags: needinfo?(mfroman)
Attachment #9572508 - Flags: sec-approval?
Group: media-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Flags: needinfo?(mfroman)
Keywords: leave-open
Resolution: --- → FIXED
Whiteboard: [reminder-test 2026-05-26]
Target Milestone: --- → 152 Branch

Please add a Beta uplift request to this bug when you get a chance. I also added a whiteboard tag to the bug that will automatically NI you about a test after Fx151 ships and has had a chance to get some uptake rather than leaving this bug open and making tracking more of a pain overall.

Flags: needinfo?(mfroman)

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: A compromised tab could hijack a user's existing camera feed, or allow the exiting feed to be disrupted.
  • 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: low
  • Explanation of risk level: Should be low given we're only adding an ownership check.
  • String changes made/needed?: n/a
  • Is Android affected?: yes
Attachment #9575480 - Flags: approval-mozilla-beta?
Attached file (secure) —

Thank you, and done.

Flags: needinfo?(mfroman)
Attachment #9575480 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c152/b151]
Whiteboard: [reminder-test 2026-05-26] → [reminder-test 2026-05-26][adv-main151+]

a month ago, RyanVM placed a reminder on the bug using the whiteboard tag [reminder-test 2026-05-26] .

mjf, please refer to the original comment to better understand the reason for the reminder.

Flags: needinfo?(mfroman)
Whiteboard: [reminder-test 2026-05-26][adv-main151+] → [adv-main151+]
Alias: CVE-2026-8972
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: