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 <[email protected]> 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 <[email protected]> Reviewed-by: Harald Alvestrand <[email protected]> Reviewed-by: Henrik Boström <[email protected]> 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 [email protected]
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