CVE-2026-14084
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fremoting/signaling/ftl_messaging_client_unittest.cc |
modified | |
TEST_Fremoting/signaling/ftl_signal_strategy_unittest.cc |
modified | |
TESTremoting/signaling/jingle_message_struct_converter_unittest.cc |
modified |
Files Changed
remoting/signaling/ftl_messaging_client.ccremoting/signaling/ftl_messaging_client_unittest.ccremoting/signaling/ftl_signal_strategy_unittest.ccremoting/signaling/jingle_message_struct_converter.ccremoting/signaling/jingle_message_struct_converter_unittest.cc
Patch
From a00ae7fd033f4e28e8a49e199bf329ff76880102 Mon Sep 17 00:00:00 2001 From: Joe Downing <[email protected]> Date: Wed, 20 May 2026 16:01:53 -0700 Subject: [PATCH] Improve DTD filter logic in SignalStrategy::ParseStanzaXml Bug: 513138148 Change-Id: I6eaed5442d6fc73f406ce374f925b3fc8c37e863 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7865584 Reviewed-by: Yuwei Huang <[email protected]> Commit-Queue: Joe Downing <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633889} --- diff --git a/remoting/signaling/ftl_messaging_client.cc b/remoting/signaling/ftl_messaging_client.cc index d419e08..4d227ac 100644 --- a/remoting/signaling/ftl_messaging_client.cc +++ b/remoting/signaling/ftl_messaging_client.cc @@ -19,9 +19,11 @@ #include "remoting/base/protobuf_http_stream_request.h" #include "remoting/signaling/ftl_message_channel_strategy.h" #include "remoting/signaling/ftl_services_context.h" +#include "remoting/signaling/jingle_message_xml_converter.h" #include "remoting/signaling/message_channel.h" #include "remoting/signaling/registration_manager.h" #include "remoting/signaling/signaling_address.h" +#include "remoting/signaling/xmpp_constants.h" #include "services/network/public/cpp/shared_url_loader_factory.h" namespace remoting { @@ -367,7 +369,19 @@ } ftl::ChromotingMessage chromoting_message; + if (message.message().length() > kMaxStanzaSize) { + LOG(ERROR) << "Rejecting FTL message: length " << message.message().length() + << " exceeds " << kMaxStanzaSize << " limit."; + return; + } chromoting_message.ParseFromString(message.message()); + + if (chromoting_message.has_xmpp() && + XmlContainsDtd(chromoting_message.xmpp().stanza())) { + LOG(ERROR) << "Rejecting XMPP message with DTD."; + return; + } + auto sender_address = is_system ? SignalingAddress::CreateFtlSystemAddress(sender_id) : SignalingAddress::CreateFtlSignalingAddress( diff --git a/remoting/signaling/ftl_messaging_client_unittest.cc b/remoting/signaling/ftl_messaging_client_unittest.cc index 4fa7a6cf..cb25be4 100644 --- a/remoting/signaling/ftl_messaging_client_unittest.cc +++ b/remoting/signaling/ftl_messaging_client_unittest.cc @@ -32,6 +32,7 @@ #include "remoting/signaling/ftl_services_context.h" #include "remoting/signaling/registration_manager.h" #include "remoting/signaling/signaling_address.h" +#include "remoting/signaling/xmpp_constants.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -111,6 +112,10 @@ void TearDown() override; protected: + void RunMessageCallbacks(const ftl::InboxMessage& message) { + messaging_client_->RunMessageCallbacks(message); + } + ProtobufHttpTestResponder test_responder_; FakeOAuthTokenGetter token_getter_{OAuthTokenGetter::Status::SUCCESS, OAuthTokenInfo()}; @@ -352,4 +357,33 @@ run_loop.Run(); } +TEST_F(FtlMessagingClientTest, RejectLargeMessage) { + base::MockCallback<FtlMessagingClient::MessageCallback> mock_on_incoming_msg; + EXPECT_CALL(mock_on_incoming_msg, Run(_, _)).Times(0); + + auto subscription = + messaging_client_->RegisterMessageCallback(mock_on_incoming_msg.Get()); + + ftl::InboxMessage inbox_message; + inbox_message.mutable_sender_id()->set_id(kFakeSenderId); + inbox_message.set_sender_registration_id(kFakeSenderRegId); + inbox_message.set_message_type( + ftl::InboxMessage_MessageType_CHROMOTING_MESSAGE); + inbox_message.set_message(std::string(kMaxStanzaSize + 1, ' ')); + + RunMessageCallbacks(inbox_message); +} + +TEST_F(FtlMessagingClientTest, RejectDtdMessage) { + base::MockCallback<FtlMessagingClient::MessageCallback> mock_on_incoming_msg; + EXPECT_CALL(mock_on_incoming_msg, Run(_, _)).Times(0); + + auto subscription = + messaging_client_->RegisterMessageCallback(mock_on_incoming_msg.Get()); + + ftl::InboxMessage inbox_message = + CreateInboxMessage(kMessage1Id, "<!DOCTYPE iq><iq/>"); + RunMessageCallbacks(inbox_message); +} + } // namespace remoting diff --git a/remoting/signaling/ftl_signal_strategy_unittest.cc b/remoting/signaling/ftl_signal_strategy_unittest.cc index af8380a..a9d1f12 100644 --- a/remoting/signaling/ftl_signal_strategy_unittest.cc +++ b/remoting/signaling/ftl_signal_strategy_unittest.cc @@ -642,6 +642,28 @@ ASSERT_EQ(received_messages_.size(), 0u); } +TEST_F(FtlSignalStrategyTest, ReceiveStanza_RejectUtf16StanzaWithDtd) { + ExpectGetOAuthTokenSucceedsWithFakeCreds(); + registration_manager_->ExpectSignInGaiaSucceeds(); + signal_strategy_->Connect(); + messaging_client_->AcceptReceivingMessages(); + + ftl::ChromotingMessage message; + // UTF-16LE encoded "<!DOCTYPE iq>" + std::string xml( + "\xFF\xFE\x3C\x00\x21\x00\x44\x00\x4F\x00\x43\x00\x54\x00\x59\x00\x50\x00" + "\x45\x00\x20\x00\x69\x00\x71\x00\x3E\x00", + 28); + message.mutable_xmpp()->set_stanza(xml); + ftl::Id remote_user_id; + remote_user_id.set_type(ftl::IdType_Type_EMAIL); + remote_user_id.set_id(kFakeRemoteUsername); + messaging_client_->OnMessage(remote_user_id, kFakeRemoteRegistrationId, + message); + + ASSERT_EQ(received_messages_.size(), 0u); +} + TEST_F(FtlSignalStrategyTest, SendMessage_Success) { ExpectGetOAuthTokenSucceedsWithFakeCreds(); registration_manager_->ExpectSignInGaiaSucceeds(); diff --git a/remoting/signaling/jingle_message_struct_converter.cc b/remoting/signaling/jingle_message_struct_converter.cc index ae8524c..b361db4 100644 --- a/remoting/signaling/jingle_message_struct_converter.cc +++ b/remoting/signaling/jingle_message_struct_converter.cc @@ -18,6 +18,7 @@ #include "remoting/signaling/jingle_message_xml_converter.h" #include "remoting/signaling/signaling_address.h" #include "remoting/signaling/signaling_id_util.h" +#include "remoting/signaling/xmpp_constants.h" #include "third_party/abseil-cpp/absl/functional/overload.h" #include "third_party/libjingle_xmpp/xmllite/xmlelement.h" @@ -505,6 +506,14 @@ [](std::monostate) { NOTREACHED(); }}, jingle_struct->action); } else { + if (stanza.xml.length() > kMaxStanzaSize) { + *error = "Rejecting XML stanza: length exceeds limit."; + return false; + } + if (XmlContainsDtd(stanza.xml)) { + *error = "Rejecting XML with DTD."; + return false; + } auto xml_stanza = base::WrapUnique<jingle_xmpp::XmlElement>( jingle_xmpp::XmlElement::ForStr(stanza.xml)); if (!xml_stanza || @@ -559,6 +568,12 @@ reply->error_type = FromErrorConditionStruct(error_struct->condition); reply->text = error_struct->text; } else { + if (stanza.xml.length() > kMaxStanzaSize) { + return false; + } + if (XmlContainsDtd(stanza.xml)) { + return false; + } auto xml_stanza = base::WrapUnique<jingle_xmpp::XmlElement>( jingle_xmpp::XmlElement::ForStr(stanza.xml)); if (!xml_stanza || !JingleMessageReplyFromXml(xml_stanza.get(), reply)) { diff --git a/remoting/signaling/jingle_message_struct_converter_unittest.cc b/remoting/signaling/jingle_message_struct_converter_unittest.cc index ae1d6359..d70a460 100644 --- a/remoting/signaling/jingle_message_struct_converter_unittest.cc +++ b/remoting/signaling/jingle_message_struct_converter_unittest.cc @@ -9,6 +9,7 @@ #include "remoting/signaling/jingle_data_structures.h" #include "remoting/signaling/signaling_address.h" +#include "remoting/signaling/xmpp_constants.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -380,5 +381,37 @@ EXPECT_TRUE(std::holds_alternative<SessionInfo>(message.payload())); } +TEST(JingleMessageStructConverterTest, RejectDtdInXmlFallback) { + internal::IqStanzaStruct stanza; + stanza.xml = "<!DOCTYPE iq><iq/>"; + JingleMessage message; + std::string error;
Regression Test / PoC
diff --git a/remoting/signaling/ftl_messaging_client_unittest.cc b/remoting/signaling/ftl_messaging_client_unittest.cc
index 4fa7a6cf..cb25be4 100644
--- a/remoting/signaling/ftl_messaging_client_unittest.cc
+++ b/remoting/signaling/ftl_messaging_client_unittest.cc
@@ -32,6 +32,7 @@
#include "remoting/signaling/ftl_services_context.h"
#include "remoting/signaling/registration_manager.h"
#include "remoting/signaling/signaling_address.h"
+#include "remoting/signaling/xmpp_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -111,6 +112,10 @@
void TearDown() override;
protected:
+ void RunMessageCallbacks(const ftl::InboxMessage& message) {
+ messaging_client_->RunMessageCallbacks(message);
+ }
+
ProtobufHttpTestResponder test_responder_;
FakeOAuthTokenGetter token_getter_{OAuthTokenGetter::Status::SUCCESS,
OAuthTokenInfo()};
@@ -352,4 +357,33 @@
run_loop.Run();
}
+TEST_F(FtlMessagingClientTest, RejectLargeMessage) {
+ base::MockCallback<FtlMessagingClient::MessageCallback> mock_on_incoming_msg;
+ EXPECT_CALL(mock_on_incoming_msg, Run(_, _)).Times(0);
+
+ auto subscription =
+ messaging_client_->RegisterMessageCallback(mock_on_incoming_msg.Get());
+
+ ftl::InboxMessage inbox_message;
+ inbox_message.mutable_sender_id()->set_id(kFakeSenderId);
+ inbox_message.set_sender_registration_id(kFakeSenderRegId);
+ inbox_message.set_message_type(
+ ftl::InboxMessage_MessageType_CHROMOTING_MESSAGE);
+ inbox_message.set_message(std::string(kMaxStanzaSize + 1, ' '));
+
+ RunMessageCallbacks(inbox_message);
+}
+
+TEST_F(FtlMessagingClientTest, RejectDtdMessage) {
+ base::MockCallback<FtlMessagingClient::MessageCallback> mock_on_incoming_msg;
+ EXPECT_CALL(mock_on_incoming_msg, Run(_, _)).Times(0);
+
+ auto subscription =
+ messaging_client_->RegisterMessageCallback(mock_on_incoming_msg.Get());
+
+ ftl::InboxMessage inbox_message =
+ CreateInboxMessage(kMessage1Id, "<!DOCTYPE iq><iq/>");
+ RunMessageCallbacks(inbox_message);
+}
+
} // namespace remoting
diff --git a/remoting/signaling/ftl_signal_strategy_unittest.cc b/remoting/signaling/ftl_signal_strategy_unittest.cc
index af8380a..a9d1f12 100644
--- a/remoting/signaling/ftl_signal_strategy_unittest.cc
+++ b/remoting/signaling/ftl_signal_strategy_unittest.cc
@@ -642,6 +642,28 @@
ASSERT_EQ(received_messages_.size(), 0u);
}
+TEST_F(FtlSignalStrategyTest, ReceiveStanza_RejectUtf16StanzaWithDtd) {
+ ExpectGetOAuthTokenSucceedsWithFakeCreds();
+ registration_manager_->ExpectSignInGaiaSucceeds();
+ signal_strategy_->Connect();
+ messaging_client_->AcceptReceivingMessages();
+
+ ftl::ChromotingMessage message;
+ // UTF-16LE encoded "<!DOCTYPE iq>"
+ std::string xml(
+ "\xFF\xFE\x3C\x00\x21\x00\x44\x00\x4F\x00\x43\x00\x54\x00\x59\x00\x50\x00"
+ "\x45\x00\x20\x00\x69\x00\x71\x00\x3E\x00",
+ 28);
+ message.mutable_xmpp()->set_stanza(xml);
+ ftl::Id remote_user_id;
+ remote_user_id.set_type(ftl::IdType_Type_EMAIL);
+ remote_user_id.set_id(kFakeRemoteUsername);
+ messaging_client_->OnMessage(remote_user_id, kFakeRemoteRegistrationId,
+ message);
+
+ ASSERT_EQ(received_messages_.size(), 0u);
+}
+
TEST_F(FtlSignalStrategyTest, SendMessage_Success) {
ExpectGetOAuthTokenSucceedsWithFakeCreds();
registration_manager_->ExpectSignInGaiaSucceeds();
diff --git a/remoting/signaling/jingle_message_struct_converter_unittest.cc b/remoting/signaling/jingle_message_struct_converter_unittest.cc
index ae1d6359..d70a460 100644
--- a/remoting/signaling/jingle_message_struct_converter_unittest.cc
+++ b/remoting/signaling/jingle_message_struct_converter_unittest.cc
@@ -9,6 +9,7 @@
#include "remoting/signaling/jingle_data_structures.h"
#include "remoting/signaling/signaling_address.h"
+#include "remoting/signaling/xmpp_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -380,5 +381,37 @@
EXPECT_TRUE(std::holds_alternative<SessionInfo>(message.payload()));
}
+TEST(JingleMessageStructConverterTest, RejectDtdInXmlFallback) {
+ internal::IqStanzaStruct stanza;
+ stanza.xml = "<!DOCTYPE iq><iq/>";
+ JingleMessage message;
+ std::string error;
+ EXPECT_FALSE(JingleMessageFromStruct(stanza, &message, &error));
+ EXPECT_THAT(error, testing::HasSubstr("Rejecting XML with DTD"));
+}
+
+TEST(JingleMessageStructConverterTest, RejectLargeXmlFallback) {
+ internal::IqStanzaStruct stanza;
+ stanza.xml = std::string(kMaxStanzaSize + 1, ' ');
+ JingleMessage message;
+ std::string error;
+ EXPECT_FALSE(JingleMessageFromStruct(stanza, &message, &error));
+ EXPECT_THAT(error, testing::HasSubstr("length exceeds limit"));
+}
+
+TEST(JingleMessageStructConverterTest, Reply_RejectDtdInXmlFallback) {
+ internal::IqStanzaStruct stanza;
+ stanza.xml = "<!DOCTYPE iq><iq/>";
+ JingleMessageReply reply;
+ EXPECT_FALSE(JingleMessageReplyFromStruct(stanza, &reply));
+}
+
+TEST(JingleMessageStructConverterTest, Reply_RejectLargeXmlFallback) {
+ internal::IqStanzaStruct stanza;
+ stanza.xml = std::string(kMaxStanzaSize + 1, ' ');
+ JingleMessageReply reply;
+ EXPECT_FALSE(JingleMessageReplyFromStruct(stanza, &reply));
+}
+
} // namespace
} // namespace remoting
diff --git a/remoting/signaling/jingle_message_xml_converter_unittest.cc b/remoting/signaling/jingle_message_xml_converter_unittest.cc
index bb33fd6..2fa8bc6 100644
--- a/remoting/signaling/jingle_message_xml_converter_unittest.cc
+++ b/remoting/signaling/jingle_message_xml_converter_unittest.cc
@@ -12,6 +12,7 @@
#include "base/strings/stringprintf.h"
#include "remoting/signaling/content_description.h"
#include "remoting/signaling/jingle_data_structures.h"
+#include "remoting/signaling/signal_strategy.h"
#include "remoting/signaling/signaling_address.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -326,4 +327,39 @@
1234);
}
+TEST(JingleMessageXmlConverterTest, XmlContainsDtd_NoDtd) {
+ EXPECT_FALSE(XmlContainsDtd("<root><child/></root>"));
+}
+
+TEST(JingleMessageXmlConverterTest, XmlContainsDtd_StandardDtd) {
+ EXPECT_TRUE(XmlContainsDtd("<!DOCTYPE root SYSTEM \"test.dtd\"><root/>"));
+}
+
+TEST(JingleMessageXmlConverterTest, XmlContainsDtd_Utf16Le) {
+ // UTF-16LE encoded "<!DOCTYPE root>"
+ std::string xml(
+ "\xFF\xFE\x3C\x00\x21\x00\x44\x00\x4F\x00\x43\x00\x54\x00\x59\x00\x50\x00"
+ "\x45\x00",
+ 20);
+ EXPECT_TRUE(XmlContainsDtd(xml));
+}
+
+TEST(JingleMessageXmlConverterTest, XmlContainsDtd_Utf16Be) {
+ // UTF-16BE encoded "<!DOCTYPE root>"
+ std::string xml(
+ "\xFE\xFF\x00\x3C\x00\x21\x00\x44\x00\x4F\x00\x43\x00\x54\x00\x59\x00\x50"
+ "\x00\x45\x00",
+ 20);
+ EXPECT_TRUE(XmlContainsDtd(xml));
+}
+
+TEST(JingleMessageXmlConverterTest, XmlContainsDtd_MixedCase) {
+ EXPECT_TRUE(XmlContainsDtd("<!doctype root><root/>"));
+}
+
+TEST(JingleMessageXmlConverterTest, ParseStanzaXml_LargeXml) {
+ std::string large_xml(64 * 1024 + 1, ' ');
+ EXPECT_FALSE(SignalStrategy::ParseStanzaXml(large_xml).has_value());
+}
+
} // namespace remoting
Original Bug Report
Potential bypass of DTD filter in SignalStrategy::ParseStanzaXml via UTF-16 encoding
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A security check in Chrome Remote Desktop that attempts to block XML stanzas containing DTDs can be bypassed by using UTF-16 encoding. While the filter performs a literal byte-search for the ASCII string <!DOCTYPE, the underlying Expat parser auto-detects UTF-16 encoding and correctly processes the DTD, re-exposing the parser to potential Denial-of-Service attacks.
Affected files:
remoting/signaling/signal_strategy.ccthird_party/libjingle_xmpp/xmllite/xmlparser.ccremoting/signaling/ftl_signal_strategy.ccremoting/signaling/ftl_messaging_client.cc
Estimated timestamp from git blame: 2026-03-31
Summary
In Chrome Remote Desktop, the SignalStrategy::ParseStanzaXml function implements a security check to reject incoming XML stanzas that contain a Document Type Definition (DTD). This check is intended to protect the privileged host and browser processes from XML-based attacks. However, because the check relies on a simple byte-literal search for the ASCII string <!DOCTYPE, it can be bypassed by encoding the XML payload in UTF-16. The subsequent XML parsing via the Expat library auto-detects the UTF-16 encoding and processes the DTD.
Vulnerability Details
The vulnerability exists in remoting/signaling/signal_strategy.cc at line 28:
if (xml.find("<!DOCTYPE") != std::string::npos) {
LOG(ERROR) << "Rejecting XML with DTD.";
return std::nullopt;
}
This check uses std::string::find, which searches for the exact ASCII byte sequence 3C 21 44 4F 43 54 59 50 45.
An attacker can bypass this check by providing a stanza encoded in UTF-16 (e.g., UTF-16LE with a Byte Order Mark). In UTF-16LE, the characters are represented as two-byte sequences, typically with null bytes (e.g., 3C 00 21 00 44 00...). The literal ASCII search will fail to find the <!DOCTYPE string in such a payload.
After passing the filter, the XML is processed by jingle_xmpp::XmlElement::ForStr(xml), which uses XmlParser. In third_party/libjingle_xmpp/xmllite/xmlparser.cc, the Expat parser is initialized with auto-detection enabled:
XmlParser::XmlParser(XmlParseHandler *pxph) : pxph_(pxph), sentError_(false) {
expat_ = XML_ParserCreate(NULL);
// ...
}
When XML_ParserCreate is called with NULL, Expat recognizes UTF-16 via the BOM or character patterns and correctly decodes the stream. Since libjingle_xmpp does not register handlers to explicitly block DTD processing (such as XML_SetStartDoctypeDeclHandler), the Expat DTD state machine is fully engaged.
Impact
This bypass re-exposes the Expat DTD state machine to remote, pre-authentication attackers. A malicious peer can send a crafted XML stanza containing an internal entity expansion payload (a ‘Billion Laughs’ attack), causing the privileged remoting_me2me_host process (on Windows, macOS, and Linux) or the ash-chrome browser process (on ChromeOS) to exhaust CPU and memory resources, leading to a Denial-of-Service.
Potential Reproduction Steps
- Identify the FTL signaling ID of a target Chrome Remote Desktop host.
- Craft an XMPP stanza encoded in UTF-16LE containing a DTD with recursive entity definitions (Billion Laughs payload), starting with a UTF-16LE BOM (
FF FE). - Send this payload to the target host via the FTL signaling service (e.g., using a Google account to call the FTL
message:sendAPI). - Observe that the
SignalStrategy::ParseStanzaXmlcheck is bypassed on the host, and the Expat parser attempts to process the malicious DTD, potentially resulting in process exhaustion or crash.
Note: These are potential steps based on code analysis; our current environment does not support executing a live proof-of-concept.
Recommended Fix
To structuraly address this issue, DTD processing should be disabled at the parser level rather than relying on string filtering:
- Register an
XML_SetStartDoctypeDeclHandlerinjingle_xmpp::XmlParserthat callsXML_StopParseror sets an error state if a DTD is encountered. - Alternatively, pin the Expat encoding to “UTF-8” in
XmlParser::XmlParserto prevent auto-detection of UTF-16 and ensure the byte-literal filter remains effective.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.