Chrome · WebRTC
CVE-2025-13639
Logic Error in WebRTC
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
SdpOfferAnswerDirectionTestpc/sdp_offer_answer_unittest.cc |
modified | |
TEST_Ppc/sdp_offer_answer_unittest.cc |
modified |
Files Changed
experiments/field_trials.pypc/peer_connection_interface_unittest.ccpc/sdp_offer_answer.ccpc/sdp_offer_answer_unittest.cc
Patch
From ff084da0f5689bb2424d662671bab6ac24eefeeb Mon Sep 17 00:00:00 2001
From: Philipp Hancke <philipp.hancke@googlemail.com>
Date: Fri, 03 Oct 2025 12:47:49 +0200
Subject: [PATCH] Improve validation of SDP direction in remote description
enabled by default and guarded by the killswitch
WebRTC-EnforceTransceiverDirection
Bug: chromium:448408148
Change-Id: I15e98d371d1b398aff4b9cb88bf34a483b36fe1b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/413340
Commit-Queue: Philipp Hancke <philipp.hancke@googlemail.com>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#45847}
---
diff --git a/experiments/field_trials.py b/experiments/field_trials.py
index 5408330..a0c7918 100755
--- a/experiments/field_trials.py
+++ b/experiments/field_trials.py
@@ -95,6 +95,9 @@
FieldTrial('WebRTC-EncoderDataDumpDirectory',
296242528,
date(2024, 4, 1)),
+ FieldTrial('WebRTC-EnforceTransceiverDirection',
+ 448408148,
+ date(2026, 6, 1)),
FieldTrial('WebRTC-ForceDtls13',
383141571,
date(2024,9,1)),
diff --git a/pc/peer_connection_interface_unittest.cc b/pc/peer_connection_interface_unittest.cc
index f77176f..7df4f6e 100644
--- a/pc/peer_connection_interface_unittest.cc
+++ b/pc/peer_connection_interface_unittest.cc
@@ -71,8 +71,10 @@
#include "pc/media_stream.h"
#include "pc/peer_connection.h"
#include "pc/peer_connection_factory.h"
+#include "pc/rtp_media_utils.h"
#include "pc/rtp_sender.h"
#include "pc/rtp_sender_proxy.h"
+#include "pc/sdp_utils.h"
#include "pc/session_description.h"
#include "pc/stream_collection.h"
#include "pc/test/fake_audio_capture_module.h"
@@ -998,8 +1000,18 @@
void CreateOfferReceiveAnswer() {
CreateOfferAsLocalDescription();
+ std::unique_ptr<SessionDescriptionInterface> offer =
+ CloneSessionDescription(pc_->local_description());
+ // Adapts the offer so that it can serve as an answer.
+ // This test does not use DTLS so direcetion does not have
+ // to be adapted in a similar way.
+ for (auto& content : offer->description()->contents()) {
+ MediaContentDescription* media_description = content.media_description();
+ media_description->set_direction(
+ RtpTransceiverDirectionReversed(media_description->direction()));
+ }
std::string sdp;
- EXPECT_TRUE(pc_->local_description()->ToString(&sdp));
+ EXPECT_TRUE(offer->ToString(&sdp));
CreateAnswerAsRemoteDescription(sdp);
}
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index f572c1a..8a70f7d 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -313,6 +313,38 @@
const SessionDescription& desc2) {
return desc1.contents().size() == desc2.contents().size();
}
+
+// Checks that the remote answer follows the rules from
+// https://datatracker.ietf.org/doc/html/rfc3264#section-6.1
+RTCError VerifyDirectionsInAnswer(const SessionDescription* local_offer,
+ const SessionDescription* remote_answer) {
+ RTC_DCHECK(local_offer);
+ RTC_DCHECK(remote_answer);
+
+ const ContentInfos& local_contents = local_offer->contents();
+ const ContentInfos& remote_contents = remote_answer->contents();
+ RTC_DCHECK(local_contents.size() == remote_contents.size());
+
+ for (size_t i = 0; i < local_contents.size(); i++) {
+ RtpTransceiverDirection local_direction =
+ local_contents[i].media_description()->direction();
+ RtpTransceiverDirection remote_direction =
+ remote_contents[i].media_description()->direction();
+
+ if (!RtpTransceiverDirectionHasRecv(local_direction) &&
+ RtpTransceiverDirectionHasSend(remote_direction)) {
+ LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+ "Incompatible receive direction");
+ }
+ if (!RtpTransceiverDirectionHasSend(local_direction) &&
+ RtpTransceiverDirectionHasRecv(remote_direction)) {
+ LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+ "Incompatible send direction");
+ }
+ }
+ return RTCError::OK();
+}
+
// Checks that each non-rejected content has a DTLS
// fingerprint, unless it's in a BUNDLE group, in which case only the
// BUNDLE-tag section (first media section/description in the BUNDLE group)
@@ -3885,6 +3917,17 @@
if (!error.ok()) {
return error;
}
+
+ if (source == CS_REMOTE &&
+ (type == SdpType::kPrAnswer || type == SdpType::kAnswer)) {
+ RTC_DCHECK(local_description());
+ error = VerifyDirectionsInAnswer(local_description()->description(),
+ sdesc->description());
+ if (!error.ok() && !env_.field_trials().IsDisabled(
+ "WebRTC-EnforceTransceiverDirection")) {
+ return error;
+ }
+ }
}
return RTCError::OK();
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index b077223..9189c3b 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -14,6 +14,7 @@
#include <memory>
#include <optional>
#include <string>
+#include <tuple>
#include <utility>
#include <vector>
@@ -47,6 +48,7 @@
#include "media/base/media_constants.h"
#include "media/base/stream_params.h"
#include "pc/peer_connection_wrapper.h"
+#include "pc/session_description.h"
#include "pc/test/fake_audio_capture_module.h"
#include "pc/test/integration_test_helpers.h"
#include "pc/test/mock_peer_connection_observers.h"
@@ -1744,4 +1746,95 @@
EXPECT_EQ(codecs[1].id, av1.id);
}
+class SdpOfferAnswerDirectionTest
+ : public SdpOfferAnswerTest,
+ public testing::WithParamInterface<
+ std::tuple<RtpTransceiverDirection, RtpTransceiverDirection, bool>> {
+ public:
+ SdpOfferAnswerDirectionTest() : SdpOfferAnswerTest() {}
+};
+
+TEST_P(SdpOfferAnswerDirectionTest, IncompatibleDirection) {
+ auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+
+ auto transceiver = caller->AddTransceiver(MediaType::VIDEO);
+ EXPECT_TRUE(transceiver->SetDirectionWithError(std::get<0>(GetParam())).ok());
+
+ auto offer = caller->CreateOfferAndSetAsLocal();
+ EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+
+ ASSERT_THAT(callee->pc()->GetTransceivers(), SizeIs(1));
+ auto callee_transceiver = callee->pc()->GetTransceivers()[0];
+ EXPECT_TRUE(callee_transceiver
+ ->SetDirectionWithError(RtpTransceiverDirection::kInactive)
+ .ok());
+ auto answer = callee->CreateAnswerAndSetAsLocal();
+ // Modify the answer.
+ ASSERT_THAT(answer->description()->contents(), SizeIs(1));
+ ContentInfo& content = answer->description()->contents()[0];
+ EXPECT_EQ(content.media_description()->direction(),
+ RtpTransceiverDirection::kInactive);
+ content.media_description()->set_direction(std::get<1>(GetParam()));
+
+ EXPECT_EQ(caller->SetRemoteDescription(std::move(answer)),
+ std::get<2>(GetParam()));
+}
+
+INSTANTIATE_TEST_SUITE_P(SdpOfferAnswerDirectionTest,
+ SdpOfferAnswerDirectionTest,
+ ::testing::Values(
+ // sendrecv.
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kSendRecv,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kSendOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kRecvOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/pc/peer_connection_interface_unittest.cc b/pc/peer_connection_interface_unittest.cc
index f77176f..7df4f6e 100644
--- a/pc/peer_connection_interface_unittest.cc
+++ b/pc/peer_connection_interface_unittest.cc
@@ -71,8 +71,10 @@
#include "pc/media_stream.h"
#include "pc/peer_connection.h"
#include "pc/peer_connection_factory.h"
+#include "pc/rtp_media_utils.h"
#include "pc/rtp_sender.h"
#include "pc/rtp_sender_proxy.h"
+#include "pc/sdp_utils.h"
#include "pc/session_description.h"
#include "pc/stream_collection.h"
#include "pc/test/fake_audio_capture_module.h"
@@ -998,8 +1000,18 @@
void CreateOfferReceiveAnswer() {
CreateOfferAsLocalDescription();
+ std::unique_ptr<SessionDescriptionInterface> offer =
+ CloneSessionDescription(pc_->local_description());
+ // Adapts the offer so that it can serve as an answer.
+ // This test does not use DTLS so direcetion does not have
+ // to be adapted in a similar way.
+ for (auto& content : offer->description()->contents()) {
+ MediaContentDescription* media_description = content.media_description();
+ media_description->set_direction(
+ RtpTransceiverDirectionReversed(media_description->direction()));
+ }
std::string sdp;
- EXPECT_TRUE(pc_->local_description()->ToString(&sdp));
+ EXPECT_TRUE(offer->ToString(&sdp));
CreateAnswerAsRemoteDescription(sdp);
}
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index b077223..9189c3b 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -14,6 +14,7 @@
#include <memory>
#include <optional>
#include <string>
+#include <tuple>
#include <utility>
#include <vector>
@@ -47,6 +48,7 @@
#include "media/base/media_constants.h"
#include "media/base/stream_params.h"
#include "pc/peer_connection_wrapper.h"
+#include "pc/session_description.h"
#include "pc/test/fake_audio_capture_module.h"
#include "pc/test/integration_test_helpers.h"
#include "pc/test/mock_peer_connection_observers.h"
@@ -1744,4 +1746,95 @@
EXPECT_EQ(codecs[1].id, av1.id);
}
+class SdpOfferAnswerDirectionTest
+ : public SdpOfferAnswerTest,
+ public testing::WithParamInterface<
+ std::tuple<RtpTransceiverDirection, RtpTransceiverDirection, bool>> {
+ public:
+ SdpOfferAnswerDirectionTest() : SdpOfferAnswerTest() {}
+};
+
+TEST_P(SdpOfferAnswerDirectionTest, IncompatibleDirection) {
+ auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+
+ auto transceiver = caller->AddTransceiver(MediaType::VIDEO);
+ EXPECT_TRUE(transceiver->SetDirectionWithError(std::get<0>(GetParam())).ok());
+
+ auto offer = caller->CreateOfferAndSetAsLocal();
+ EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+
+ ASSERT_THAT(callee->pc()->GetTransceivers(), SizeIs(1));
+ auto callee_transceiver = callee->pc()->GetTransceivers()[0];
+ EXPECT_TRUE(callee_transceiver
+ ->SetDirectionWithError(RtpTransceiverDirection::kInactive)
+ .ok());
+ auto answer = callee->CreateAnswerAndSetAsLocal();
+ // Modify the answer.
+ ASSERT_THAT(answer->description()->contents(), SizeIs(1));
+ ContentInfo& content = answer->description()->contents()[0];
+ EXPECT_EQ(content.media_description()->direction(),
+ RtpTransceiverDirection::kInactive);
+ content.media_description()->set_direction(std::get<1>(GetParam()));
+
+ EXPECT_EQ(caller->SetRemoteDescription(std::move(answer)),
+ std::get<2>(GetParam()));
+}
+
+INSTANTIATE_TEST_SUITE_P(SdpOfferAnswerDirectionTest,
+ SdpOfferAnswerDirectionTest,
+ ::testing::Values(
+ // sendrecv.
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kSendRecv,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kSendOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kRecvOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendRecv,
+ RtpTransceiverDirection::kInactive,
+ true),
+ // sendonly.
+ std::make_tuple(RtpTransceiverDirection::kSendOnly,
+ RtpTransceiverDirection::kSendRecv,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kSendOnly,
+ RtpTransceiverDirection::kSendOnly,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kSendOnly,
+ RtpTransceiverDirection::kRecvOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kSendOnly,
+ RtpTransceiverDirection::kInactive,
+ true),
+ // recvonly.
+ std::make_tuple(RtpTransceiverDirection::kRecvOnly,
+ RtpTransceiverDirection::kSendRecv,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kRecvOnly,
+ RtpTransceiverDirection::kSendOnly,
+ true),
+ std::make_tuple(RtpTransceiverDirection::kRecvOnly,
+ RtpTransceiverDirection::kRecvOnly,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kRecvOnly,
+ RtpTransceiverDirection::kInactive,
+ true),
+ // inactive.
+ std::make_tuple(RtpTransceiverDirection::kInactive,
+ RtpTransceiverDirection::kSendRecv,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kInactive,
+ RtpTransceiverDirection::kSendOnly,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kInactive,
+ RtpTransceiverDirection::kRecvOnly,
+ false),
+ std::make_tuple(RtpTransceiverDirection::kInactive,
+ RtpTransceiverDirection::kInactive,
+ true)));
+
} // namespace webrtc
Loading diff…
Original Bug Report
reported by ph...@googlemail.com
SetRemoteDescription does not validate transceiver direction properly
VULNERABILITY DETAILS https://jsfiddle.net/e7t531oc/ shows that setRemoteDescription allows the remote SDP to control whether the local side receives and processes packets. See https://issues.webrtc.org/issues/42221095 for previous issues preventing that.
Firefox rejects this. VERSION Chrome Version: 140.0.0.0 + [stable, beta, and dev] Operating System: all
REPRODUCTION CASE https://jsfiddle.net/e7t531oc/
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Philipp Hancke
References
On This Page