Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebRTC
DescriptionInsufficient validation of untrusted input in WebRTC
ComponentWebRTC
Bug ClassLogic Error
Tracker504572664
Fix commitaa54326daa32 (src) +82/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
p2p/base/turn_port_unittest.cc
modified

Files Changed

  • p2p/base/stun_request.cc
  • p2p/base/turn_port_unittest.cc
From aa54326daa32892de997bb6fd60701b8e03c718a Mon Sep 17 00:00:00 2001
From: Harald Alvestrand <hta@webrtc.org>
Date: Wed, 22 Apr 2026 20:16:27 +0000
Subject: [PATCH] Discard unauthenticated TURN responses to unauthenticated requests

In StunRequestManager::CheckResponse, if a TURN request was sent
without authentication (initial ALLOCATE), we now explicitly discard
any success response. We also restrict allowed error responses to
401 Unauthorized and 300 Try Alternate.

This prevents an authentication bypass (b/504572664) where an attacker
could hijack a TURN session by forging a success response to the initial
unauthenticated request.

Add regression test for unauthenticated TURN ALLOCATE success.

Bug: chromium:504572664
Change-Id: Ie5619e10b6b6dccac3221661632e9f1d6312f760
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/466100
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47529}
---

diff --git a/p2p/base/stun_request.cc b/p2p/base/stun_request.cc
index 3afabe1..1a18d2b 100644
--- a/p2p/base/stun_request.cc
+++ b/p2p/base/stun_request.cc
@@ -32,6 +32,7 @@
 #include "rtc_base/checks.h"
 #include "rtc_base/logging.h"
 #include "rtc_base/string_encode.h"
+#include "rtc_base/string_utils.h"
 
 namespace webrtc {
 
@@ -122,8 +123,28 @@
   bool skip_integrity_checking =
       (request->msg()->integrity() == StunMessage::IntegrityStatus::kNotSet);
   if (!request->AuthenticationRequired()) {
-    // This is a STUN_BINDING to from stun_port.cc or
-    // the initial (unauthenticated) TURN_ALLOCATE_REQUEST.
+    if (request->type() != STUN_BINDING_REQUEST) {
+      if (msg->type() == GetStunSuccessResponseType(request->type())) {
+        RTC_LOG(LS_WARNING)
+            << "Discarding unauthenticated success response (0x"
+            << ToHex(msg->type()) << ") to TURN request of type 0x"
+            << ToHex(request->type())
+            << ", id=" << hex_encode(msg->transaction_id());
+        return false;
+      }
+      if (msg->type() == GetStunErrorResponseType(request->type())) {
+        int error_code = msg->GetErrorCodeValue();
+        if (error_code != STUN_ERROR_UNAUTHORIZED &&
+            error_code != STUN_ERROR_TRY_ALTERNATE) {
+          RTC_LOG(LS_WARNING)
+              << "Discarding unauthenticated error response with code "
+              << error_code << " to TURN request of type 0x"
+              << ToHex(request->type())
+              << ", id=" << hex_encode(msg->transaction_id());
+          return false;
+        }
+      }
+    }
   } else if (skip_integrity_checking) {
     // TODO(chromium:1177125): Remove below!
     // This indicates lazy test writing (not adding integrity attribute).
diff --git a/p2p/base/turn_port_unittest.cc b/p2p/base/turn_port_unittest.cc
index 8a53aae..b23f33a 100644
--- a/p2p/base/turn_port_unittest.cc
+++ b/p2p/base/turn_port_unittest.cc
@@ -1948,6 +1948,65 @@
   unsigned int* attr_counter_ = nullptr;
 };
 
+// Test that an unauthenticated TURN ALLOCATE success response is NOT accepted
+// if integrity is expected but not present.
+// This is a regression test for b/504572664.
+TEST_F(TurnPortTest, TestUnauthenticatedAllocateSuccessRejected) {
+  SocketAddress fake_server_addr("99.99.99.99", 3478);
+  CreateTurnPort(kTurnUsername, kTurnPassword,
+                 ProtocolAddress(fake_server_addr, PROTO_UDP));
+
+  std::unique_ptr<AsyncPacketSocket> server_socket =
+      socket_factory()->CreateUdpSocket(env_, fake_server_addr, 0, 0);
+
+  std::string transaction_id;
+  server_socket->RegisterReceivedPacketCallback(
+      [&](AsyncPacketSocket* /* socket */, const ReceivedIpPacket& packet) {
+        ByteBufferReader reader(packet.payload());
+        TurnMessage msg;
+        if (msg.Read(&reader) && msg.type() == STUN_ALLOCATE_REQUEST) {
+          transaction_id = msg.transaction_id();
+        }
+      });
+
+  turn_port_->PrepareAddress();
+
+  // Wait for the request to reach the server.
+  ASSERT_TRUE(
+      WaitUntil([&] { return !transaction_id.empty(); },
+                {.timeout = kSimulatedRtt, .clock = &time_controller_}));
+
+  TurnMessage forged_success(STUN_ALLOCATE_RESPONSE, transaction_id);
+
+  // Add required attributes for ALLOCATE success.
+  SocketAddress relayed_addr("198.51.100.99", 6666);
+  SocketAddress mapped_addr("203.0.113.77", 5555);
+
+  forged_success.AddAttribute(std::make_unique<StunXorAddressAttribute>(
+      STUN_ATTR_XOR_RELAYED_ADDRESS, relayed_addr));
+  forged_success.AddAttribute(std::make_unique<StunXorAddressAttribute>(
+      STUN_ATTR_XOR_MAPPED_ADDRESS, mapped_addr));
+  forged_success.AddAttribute(
+      std::make_unique<StunUInt32Attribute>(STUN_ATTR_LIFETIME, 300));
+
+  ByteBufferWriter buf;
+  forged_success.Write(&buf);
+
+  // Send the forged response to the TurnPort.
+  SocketAddress local_addr = turn_port_->socket()->GetLocalAddress();
+  AsyncSocketPacketOptions local_options;
+  server_socket->SendTo(buf.Data(), buf.Length(), local_addr, local_options);
+
+  // Wait a bit for the packet to be processed.
+  time_controller_.AdvanceTime(kSimulatedRtt);
+
+  // If vulnerable, turn_ready_ would be true because it accepted the forged
+  // success. The correct behavior is to reject it and eventually fail or
+  // receive the 401.
+  EXPECT_FALSE(turn_ready_)
+      << "Vulnerability present: Unauthenticated ALLOCATE success accepted!";
+}
+
 // Do a TURN allocation, establish a TLS connection, and send some data.
 // Add customizer and check that it get called.
 TEST_F(TurnPortTest, TestTurnCustomizerCount) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/p2p/base/turn_port_unittest.cc b/p2p/base/turn_port_unittest.cc
index 8a53aae..b23f33a 100644
--- a/p2p/base/turn_port_unittest.cc
+++ b/p2p/base/turn_port_unittest.cc
@@ -1948,6 +1948,65 @@
   unsigned int* attr_counter_ = nullptr;
 };
 
+// Test that an unauthenticated TURN ALLOCATE success response is NOT accepted
+// if integrity is expected but not present.
+// This is a regression test for b/504572664.
+TEST_F(TurnPortTest, TestUnauthenticatedAllocateSuccessRejected) {
+  SocketAddress fake_server_addr("99.99.99.99", 3478);
+  CreateTurnPort(kTurnUsername, kTurnPassword,
+                 ProtocolAddress(fake_server_addr, PROTO_UDP));
+
+  std::unique_ptr<AsyncPacketSocket> server_socket =
+      socket_factory()->CreateUdpSocket(env_, fake_server_addr, 0, 0);
+
+  std::string transaction_id;
+  server_socket->RegisterReceivedPacketCallback(
+      [&](AsyncPacketSocket* /* socket */, const ReceivedIpPacket& packet) {
+        ByteBufferReader reader(packet.payload());
+        TurnMessage msg;
+        if (msg.Read(&reader) && msg.type() == STUN_ALLOCATE_REQUEST) {
+          transaction_id = msg.transaction_id();
+        }
+      });
+
+  turn_port_->PrepareAddress();
+
+  // Wait for the request to reach the server.
+  ASSERT_TRUE(
+      WaitUntil([&] { return !transaction_id.empty(); },
+                {.timeout = kSimulatedRtt, .clock = &time_controller_}));
+
+  TurnMessage forged_success(STUN_ALLOCATE_RESPONSE, transaction_id);
+
+  // Add required attributes for ALLOCATE success.
+  SocketAddress relayed_addr("198.51.100.99", 6666);
+  SocketAddress mapped_addr("203.0.113.77", 5555);
+
+  forged_success.AddAttribute(std::make_unique<StunXorAddressAttribute>(
+      STUN_ATTR_XOR_RELAYED_ADDRESS, relayed_addr));
+  forged_success.AddAttribute(std::make_unique<StunXorAddressAttribute>(
+      STUN_ATTR_XOR_MAPPED_ADDRESS, mapped_addr));
+  forged_success.AddAttribute(
+      std::make_unique<StunUInt32Attribute>(STUN_ATTR_LIFETIME, 300));
+
+  ByteBufferWriter buf;
+  forged_success.Write(&buf);
+
+  // Send the forged response to the TurnPort.
+  SocketAddress local_addr = turn_port_->socket()->GetLocalAddress();
+  AsyncSocketPacketOptions local_options;
+  server_socket->SendTo(buf.Data(), buf.Length(), local_addr, local_options);
+
+  // Wait a bit for the packet to be processed.
+  time_controller_.AdvanceTime(kSimulatedRtt);
+
+  // If vulnerable, turn_ready_ would be true because it accepted the forged
+  // success. The correct behavior is to reject it and eventually fail or
+  // receive the 401.
+  EXPECT_FALSE(turn_ready_)
+      << "Vulnerability present: Unauthenticated ALLOCATE success accepted!";
+}
+
 // Do a TURN allocation, establish a TLS connection, and send some data.
 // Add customizer and check that it get called.
 TEST_F(TurnPortTest, TestTurnCustomizerCount) {
Loading diff…

Original Bug Report

reported by vm...@google.com

TURN session hijack via forged unauthenticated ALLOCATE success response

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 logic flaw in WebRTC’s STUN/TURN handling allows an on-path attacker to forge a success response to an initial, unauthenticated TURN ALLOCATE request. This bypasses the normal 401 Unauthorized challenge, leaving the port’s credential hash empty for all subsequent requests. The attacker can then impersonate the TURN relay indefinitely by using an empty key to compute MESSAGE-INTEGRITY hashes.

Affected files:

  • third_party/webrtc/p2p/base/stun_request.cc
  • third_party/webrtc/p2p/base/turn_port.cc
  • third_party/webrtc/api/transport/stun.cc
  • third_party/webrtc/rtc_base/message_digest.cc

Estimated timestamp from git blame: 2023-11-15

Summary

A logic flaw exists in StunRequestManager::CheckResponse that allows an active on-path attacker to bypass integrity checks for TURN ALLOCATE success responses. By winning a race against the legitimate TURN server’s response to an initial unauthenticated request, an attacker can supply forged relay and mapped addresses. Because the authentication challenge is bypassed, subsequent TURN requests use an empty HMAC key for MESSAGE-INTEGRITY, allowing the attacker to fully impersonate the TURN server for the duration of the session.

Root Cause Analysis

In WebRTC, a TurnPort starts by sending an unauthenticated TURN ALLOCATE request to discover the required realm and nonce. This request is marked with SetAuthenticationRequired(false) because no credentials have been negotiated yet.

In third_party/webrtc/p2p/base/stun_request.cc, the CheckResponse function determines whether to skip integrity validation based on the outgoing request:

bool skip_integrity_checking =
    (request->msg()->integrity() == StunMessage::IntegrityStatus::kNotSet);

For the initial unauthenticated request, skip_integrity_checking is set to true and AuthenticationRequired() is false. When an attacker sends a forged Allocate Success (0x0103) response, the code skips the ValidateMessageIntegrity block and evaluates:

} else if (msg->type() == GetStunSuccessResponseType(request->type())) {
  if (!msg->IntegrityOk() && !skip_integrity_checking) {
    return false;
  }
  // ...
  owned_request->OnResponse(msg);
  return true;
}

Because skip_integrity_checking is true, the success response is accepted without a valid MESSAGE-INTEGRITY attribute. The pending request is then erased, meaning the legitimate server’s subsequent 401 Unauthorized challenge will be silently dropped.

Because the 401 challenge is dropped, TurnPort::set_realm and TurnPort::UpdateHash are never called, leaving TurnPort::hash_ empty. In Release builds (NDEBUG), the RTC_DCHECK(!hash_.empty()) in TurnPort::AddRequestAuthInfo is compiled out. Subsequent TURN requests (Refresh, CreatePermission) will compute their MESSAGE-INTEGRITY HMAC using this empty key, which ComputeHmac handles by zero-padding.

Potential Exploitation Steps

(Note: These are potential steps based on code analysis; a working PoC has not been executed.)

  1. Intercept: An active on-path attacker observes the cleartext UDP TURN ALLOCATE request and extracts its 12-byte Transaction ID.
  2. Forge: The attacker races the legitimate TURN server by sending a spoofed UDP STUN Allocate Success response (0x0103) containing the intercepted Transaction ID and attacker-controlled XOR-RELAYED-ADDRESS values.
  3. Bypass: WebRTC processes the response, bypassing the integrity check because the original request was unauthenticated. The application surfaces the attacker’s relay candidate to the remote peer.
  4. Hijack: Subsequent TURN Refresh requests sent by the browser use an empty key for their MESSAGE-INTEGRITY attribute. The attacker observes these, validates the empty-key HMAC, and responds with successful responses using the same empty key, maintaining the hijacked session indefinitely.

Impact

An active on-path attacker can force the client to use an attacker-controlled TURN relay. This allows the attacker to intercept, drop, or manipulate the routing of peer-to-peer traffic. While media content remains protected by end-to-end DTLS-SRTP encryption, this breaks the security guarantees of the TURN protocol itself.

Suggested Fix

The system should strictly enforce state transitions for TURN allocations. Specifically:

  1. If a TurnAllocateRequest is sent without credentials but the TurnPort has been configured with a username and password, it should explicitly reject Allocate Success responses and only accept a 401 Unauthorized challenge.
  2. Alternatively, StunRequestManager::CheckResponse should be hardened so that skip_integrity_checking does not automatically bypass validation for Success responses if the protocol dictates that a Success response must always be authenticated once a session is established.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

Raised in root component due to access or custom field issues on 1565889

View on issue tracker