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.cppSource/WebCore/Modules/mediastream/RTCEncodedStreamProducer.hSource/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cppSource/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
Test Files
LayoutTests/http/wpt/webrtc/audio-video-transform.jsLayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txtLayoutTests/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;