Medium CVSS 6.5 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionVideo frames could have been leaked between origins in some situations.
ComponentDOM
Bug ClassLogic Error
Tracker1920800
Fix commit734c59fe8154 (firefox) +6/-11
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedKarl Tomlinson
Disclosed2024-10-29

Background

`VideoFrameContainer`
The DOM media object that holds the sequence of decoded video Images destined for a <video> element and tracks the security PrincipalHandle that owns those frames.
`PrincipalHandle`
An opaque token identifying the origin (principal) that produced a set of media frames, used to enforce cross-origin restrictions such as tainting a <canvas> drawn from a <video>.
`ImageContainer::FrameID`
A monotonically increasing identifier assigned to each frame pushed into the container, used to order frames and to reason about which frames precede a principal-handle transition.
`mFrameIDForPendingPrincipalHandle`
The FrameID at which a newly pending PrincipalHandle takes effect, i.e. the first frame that belongs to the new principal.

Root Cause Analysis

When a media source changes origin, VideoFrameContainer::SetCurrentFramesLocked stores the new PrincipalHandle in mPendingPrincipalHandle and records mFrameIDForPendingPrincipalHandle as the first FrameID that belongs to it; the code must not promote the pending handle to mLastPrincipalHandle until every frame produced under the old principal has actually drained out of the container. The buggy condition computed lastFrameIDForOldPrincipalHandle = mFrameIDForPendingPrincipalHandle - 1 and promoted the new principal whenever the last old image OR the first new image reached that boundary, which could fire while frames belonging to the old (or a mismatched) principal were still present in the set of images being displayed. This violated the invariant that the notified principal must strictly cover all currently visible frames, so a frame could be attributed to, and rendered under, the wrong origin.

The fix replaces the fragile off-by-one boundary math with a direct check: the pending handle is promoted only when aImages is empty or its first frame’s mFrameID is at least mFrameIDForPendingPrincipalHandle, guaranteeing no earlier-principal frames remain. Because the new test keys off the actual first frame of the incoming set rather than a derived - 1 boundary, the old principal’s frames are provably flushed before the origin transition is announced.

Key insight
The single mistake was using an off-by-one derived boundary (mFrameIDForPendingPrincipalHandle - 1) combined with an over-permissive OR condition to decide when the old principal was flushed, letting the origin change be announced while old-origin frames were still live; the fix eliminates the derived boundary and requires that the first surviving frame’s mFrameID be >= mFrameIDForPendingPrincipalHandle.

Attack Path

  1. Set up a cross-origin video source An attacker page loads or feeds a <video>/media element whose stream transitions between two different origins, causing a pending PrincipalHandle change with a specific mFrameIDForPendingPrincipalHandle.
  2. Trigger the frame update Frames are pushed through SetCurrentFramesLocked so that the last old image or the first new image lands exactly on the lastFrameIDForOldPrincipalHandle boundary while old-principal frames remain in the displayed set.
  3. Force premature principal promotion The flawed OR condition promotes mPendingPrincipalHandle to mLastPrincipalHandle even though a frame from the previous origin is still current.
  4. Capture the leaked frame The attacker draws the <video> to a <canvas> and reads it back (e.g. getImageData), obtaining pixel data from the other origin because the container reported the frame as belonging to the attacker-controlled principal.

Impact Assessment

An attacker obtains cross-origin video frame contents (pixel data) that should have been protected by same-origin policy, running in the content process context of a web page. The preconditions are the ability to script a media element whose principal transitions between origins and to time frame delivery so a frame crosses the mis-handled FrameID boundary; the disclosure is limited to leaked frames, giving it a moderate severity rating.

Files Changed

  • dom/media/VideoFrameContainer.cpp

Audit Directions

  • Off-by-one boundary math on ordered IDs
    Review any logic that derives a threshold via id - 1 (or + 1) to gate a security-relevant state transition, since such derived boundaries frequently admit frames/events on the wrong side of the transition.
  • Principal/origin transition gating
    Audit other media and stream code that buffers frames across a PrincipalHandle change to confirm the new principal is announced only after all older-principal data is provably flushed.
  • Over-permissive OR conditions
    Scan for guard clauses that OR together “old set” and “new set” tests where a stricter AND/coverage check is required, as satisfying either branch alone can leak state before the intended invariant holds.
diff --git a/dom/media/VideoFrameContainer.cpp b/dom/media/VideoFrameContainer.cpp
index 22ecb995094..f7471cdcca2 100644
--- a/dom/media/VideoFrameContainer.cpp
+++ b/dom/media/VideoFrameContainer.cpp
@@ -152,18 +152,13 @@ void VideoFrameContainer::SetCurrentFramesLocked(
   mImageContainer->GetCurrentImages(&oldImages);
 
   PrincipalHandle principalHandle = PRINCIPAL_HANDLE_NONE;
-  ImageContainer::FrameID lastFrameIDForOldPrincipalHandle =
-      mFrameIDForPendingPrincipalHandle - 1;
   if (mPendingPrincipalHandle != PRINCIPAL_HANDLE_NONE &&
-      ((!oldImages.IsEmpty() &&
-        oldImages.LastElement().mFrameID >= lastFrameIDForOldPrincipalHandle) ||
-       (!aImages.IsEmpty() &&
-        aImages[0].mFrameID > lastFrameIDForOldPrincipalHandle))) {
-    // We are releasing the last FrameID prior to
-    // `lastFrameIDForOldPrincipalHandle` OR there are no FrameIDs prior to
-    // `lastFrameIDForOldPrincipalHandle` in the new set of images. This means
-    // that the old principal handle has been flushed out and we can notify our
-    // video element about this change.
+      (aImages.IsEmpty() ||
+       aImages[0].mFrameID >= mFrameIDForPendingPrincipalHandle)) {
+    // There are no FrameIDs prior to `mFrameIDForPendingPrincipalHandle`
+    // in the new set of images.
+    // This means that the old principal handle has been flushed out and we
+    // can notify our video element about this change.
     principalHandle = mPendingPrincipalHandle;
     mLastPrincipalHandle = mPendingPrincipalHandle;
     mPendingPrincipalHandle = PRINCIPAL_HANDLE_NONE;
Loading diff…