CamerasParent::RecvAllocateCapture bypasses permission via IsWindowCapturing
Categories
(Core :: WebRTC: Audio/Video, defect)
Tracking
()
| 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.
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
| Reporter | ||
Comment 4•5 months ago
|
||
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.
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 5•5 months ago
|
||
Comment 6•5 months ago
|
||
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?
Updated•5 months ago
|
Comment 7•5 months ago
|
||
(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.
| Assignee | ||
Comment 9•5 months ago
|
||
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.
| Assignee | ||
Comment 10•5 months ago
|
||
Adding leave-open so I remember to come back to check on how reasonable it would be to add a test.
Comment 11•5 months ago
|
||
(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.
| Assignee | ||
Comment 13•5 months ago
|
||
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
Comment 14•5 months ago
|
||
Comment on attachment 9572508 [details]
(secure)
sec-low bugs don't need sec-approval to land. Go ahead and push.
Updated•5 months ago
|
Comment 15•5 months ago
|
||
Comment 16•5 months ago
|
||
Comment 17•5 months ago
|
||
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.
Comment 18•5 months ago
|
||
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
| Assignee | ||
Comment 19•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D295857
Updated•5 months ago
|
Updated•5 months ago
|
Comment 21•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 22•4 months ago
|
||
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.
Updated•4 months ago
|
Updated•2 months ago
|
Updated•1 month ago
|
Description
•