CVE-2026-28944

Overview

Advisory: Apple Security Advisory

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Kenneth Hsu of Palo Alto Networks, Jérôme DJOUDER, dr3dd

Attribute Value
CVE CVE-2026-28944
Bugzilla 311131
Component WebCore
Bug Class LogicError
Severity medium
Commit 8384c8455e7b5bc4…
Advisory Apple Advisory

Root Cause Analysis

WebRTC encoded stream transformer allowed writing audio frames to video streams and vice versa. RTCEncodedStreamProducer::writeFrame did not validate that the incoming frame type matched the stream type. Fix: Added m_hasTransformer and m_transformer weak pointer checks; writeFrame now returns early if m_isVideo != isVideo or if the frame is not from the expected transformer.

Attack Path

1. Step 1

Attacker establishes a WebRTC peer connection with encoded transform.

2. Step 2

Attacker intercepts or injects encoded frames via RTCRtpScriptTransform.

3. Step 3

writeFrame receives a frame of wrong type (audio vs video).

4. Step 4

Frame is written to incorrect stream without validation.

5. Step 5

Media pipeline corruption, decoder crash, or potentially memory corruption.

Files Changed

Source Files

  • Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
  • Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h
  • Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
  • Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h

Test Files

  • LayoutTests/http/wpt/webrtc/audio-video-transform.js
  • LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
  • LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html

Patch Preview

diff --git a/LayoutTests/http/wpt/webrtc/audio-video-transform.js b/LayoutTests/http/wpt/webrtc/audio-video-transform.js
index 46f520b50530..565591b39f12 100644
--- a/LayoutTests/http/wpt/webrtc/audio-video-transform.js
+++ b/LayoutTests/http/wpt/webrtc/audio-video-transform.js
@@ -1,3 +1,7 @@
+var audioSenderTransformer, videoSenderTransformer;
+var audioReceiverTransformer, videoReceiverTransformer;
+var audioChunk, videoChunk;
+
 class AudioVideoRTCRtpTransformer {
     constructor(transformer) {
         this.askKeyFrame = false;
@@ -17,7 +21,24 @@ class AudioVideoRTCRtpTransformer {
                 this.tryAccessingDataTwice = true;
             else if (event.data === "tryAccessingMetadata")
                 this.tryAccessingMetadata = true;
+            else if (event.data === "tryWritingAudio")
+                this.tryWritingAudio = true;
+            else if (event.data === "tryWritingVideo")
+                this.tryWritingVideo = true;
         };
+
+        if (this.context.options.side === "sender") {
+            if (this.context.options.mediaType === "audio")
+                audioSenderTransformer = this;
+            else if (this.context.options.mediaType === "video")
+                videoSenderTransformer = this;
+        } else {
+            if (this.context.options.mediaType === "audio")
+                audioReceiverTransformer = this;
+            else if (this.context.options.mediaType === "video")
+                videoReceiverTransformer = this;
+        }
+
         this.start();
     }
     start()
@@ -29,10 +50,98 @@ class AudioVideoRTCRtpTransformer {
 
     process()
     {
-        this.reader.read().then(chunk => {
+        this.reader.read().then(async chunk => {
             if (chunk.done)
                 return;
 
+            if (audioSenderTransformer && audioSenderTransformer.tryWritingVideo) {
+                if (audioSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (videoChunk !== undefined) {
+                       this.writer.write(videoChunk.value);
+                       audioSenderTransformer.tryWritingVideo = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if(videoSenderTransformer === this) {
+                    videoChunk = chunk;
+                    while (audioSenderTransformer.tryWritingVideo)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    videoSenderTransformer.writer.write(videoChunk);
+                    videoChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (videoSenderTransformer && videoSenderTransformer.tryWritingAudio) {
+                if (videoSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (audioChunk !== undefined) {
+                       this.writer.write(audioChunk.value);
+                       videoSenderTransformer.tryWritingAudio = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if(audioSenderTransformer === this) {
+                    audioChunk = chunk;
+                    while (videoSenderTransformer.tryWritingAudio)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    audioSenderTransformer.writer.write(audioChunk);
+                    audioChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (audioSenderTransformer && audioSenderTransformer.tryWritingAudio) {
+                if (audioSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (audioChunk !== undefined) {
+                       this.writer.write(audioChunk.value);
+                       audioSenderTransformer.tryWritingAudio = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;