All vulnerabilities
GHSA-g4wg-67xq-8q53

FreeRDP: Heap-buffer-overflow read in proxy DRDYNVC channel-name handling (DynvcTrackerHandleCreateBack)

FreeRDP
CVSS
6.5Medium
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 heap-buffer-overflow read (CWE-125) exists in FreeRDP's proxy component (server/proxy/channels/pf_channel_drdynvc.c, DynvcTrackerHandleCreateBack()). A malicious or compromised backend RDP server can trigger an out-of-bounds heap read in the freerdp-proxy process by sending a DRDYNVC CREATE_REQUEST_PDU whose channel name contains no NUL byte within its reassembled length. Severity is low: impact is a bounded local-log memory disclosure (up to ~63 bytes) or a process crash. There is no write primitive and no code-execution path.

Details

static PfChannelResult DynvcTrackerHandleCreateBack(ChannelStateTracker* tracker, wStream* s,
                                                    DWORD flags, proxyData* pdata,
                                                    pServerDynamicChannelContext* dynChannel,
                                                    DynChannelContext* dynChannelContext,
                                                    UINT64 dynChannelId)
{
	proxyChannelDataEventInfo dev = WINPR_C_ARRAY_INIT;
	const char* name = Stream_ConstPointer(s);
	const size_t nameLen = Stream_GetRemainingLength(s);
	const size_t len = strnlen(name, nameLen);
	...
	if ((len == 0) || (len == nameLen) || (dynChannelId > UINT16_MAX))
	{
		char namebuffer[64] = WINPR_C_ARRAY_INIT;
		(void)_snprintf(namebuffer, sizeof(namebuffer) - 1, "%s", name);

		DynvcTrackerLog(dynChannelContext->log, WLOG_ERROR, dynChannel, cmd, isBackData,
		                "channel id %" PRIu64 ", name=%s [...]",
		                dynChannelId, namebuffer, len, nameLen, ...);
		return PF_CHANNEL_RESULT_ERROR;
	}

(server/proxy/channels/pf_channel_drdynvc.c, confirmed present at these lines in FreeRDP 3.31.1 / commit 63b948ca5cb94307fd5444ee6e73927a41ccdab4. File carries a 2022 copyright; we have not determined how far back the defect itself goes.)

name points into ChannelStateTracker.currentPacket, the reassembled dynamic-channel-creation buffer, populated via Stream_New/Stream_EnsureRemainingCapacity/Stream_Write as PDU bytes arrive from the wire. strnlen(name, nameLen) == nameLen is exactly the signal that **no NUL byte exists anywhere in the nameLen valid bytes**, and this is precisely the branch that then calls _snprintf(namebuffer, sizeof(namebuffer) - 1, "%s", name).

_snprintf is WinPR's macro for plain glibc snprintf (winpr/include/winpr/string.h). An unprecisioned %s conversion (no %.*s) requires glibc's vfprintf to internally compute strlen(name); that scan is bounded only by where it finds a NUL byte, not by nameLen or sizeof(namebuffer). Since the preceding check already proved no NUL exists within the valid region, the scan necessarily runs past the end of the Stream_EnsureRemainingCapacity heap allocation.

Reachability, traced through the full call chain:

  • Reached via pf_dynvc_back_data/channel->onBackData: the direction for bytes received from the backend RDP server the proxy connects to (isBackData == TRUE, "B->F" per the code's own getDirection()). - DynvcTrackerHandleCreate requires lastPacket == TRUE before dispatching here, so name/nameLen reflect a complete, fully reassembled CREATE_REQUEST_PDU.
  • The "unknown channel id" guard elsewhere in the state machine is explicitly bypassed for this exact (CREATE_REQUEST_PDU, isBackData) combination by design, a brand-new channel ID is the expected, normal case here, so nothing rejects the packet on that basis.
  • No check anywhere earlier in the parse path (the channel-id varint decode, the lastPacket gate) inspects the channel-name bytes for a NUL or bounds their length before the strnlen call shown above.
  • The read is unconditional: it happens the instant the if branch is entered, before any log-level check, it does not depend on the resulting WLOG_ERROR message actually being emitted or read.

We separately checked the rest of the FreeRDP codebase for the same pattern (strnlen proving non-termination, then an unprecisioned %s-consuming call on the same buffer) and found no other live instance, every other site we checked already uses a safe idiom (an explicit reject when the wire-declared terminator position isn't actually NUL, or a guaranteed-terminator allocation). This appears to be an isolated oversight in this file, not a systemic issue.

PoC

Configuration: default build (WITH_PROXY=ON, the default), running freerdp-proxy configured to relay a session to a backend server the proxy operator does not fully trust (the threat model that motivates proxy's own existence, e.g. brokering access to third-party or customer-owned RDP servers).

Wire-level trigger, sent by the backend server as a single, unfragmented DRDYNVC PDU (flags = CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST):

byte0   = 0x10        // high nibble 0x1 = CREATE_REQUEST_PDU; low 2 bits (cbId) = 00 -> 1-byte channel id
byte1   = <any value not already an open channel id>
byte2.. = "AAAA...A"  // channel-name bytes, length >= 1, containing NO 0x00 byte anywhere

No other step in the parser rejects this: the channel-id varint decode succeeds trivially (structural validity only), the "unknown channel" guard is bypassed by design for a fresh CREATE_REQUEST_PDU, and lastPacket is satisfied by construction (single, complete PDU).

Isolated memory-safety confirmation (AddressSanitizer): we did not stand up a full live two-sided proxy session (real client + real backend + the freerdp-proxy binary) for this report; the reachability above is established by source-level trace. To confirm the memory-safety claim itself, we built a minimal harness using FreeRDP's own winpr Stream_New/Stream_EnsureRemainingCapacity/Stream_Write allocator (linked against the project's real, unmodified libwinpr3.so) and the literal vulnerable lines quoted above, verbatim. AddressSanitizer reports a genuine heap-buffer-overflow READ, reproduced independently at two buffer sizes:

==5692==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x511000000140 ...
READ of size 267 at 0x511000000140 thread T0
    #0 in printf_common .../sanitizer_common_interceptors_format.inc:563
    #1 in vsnprintf .../sanitizer_common_interceptors.inc:1652
    #2 in snprintf .../sanitizer_common_interceptors.inc:1723
    #3 in run_case harness.c:114
0x511000000140 is located 0 bytes after 256-byte region [0x511000000040,0x511000000140)
allocated by thread T0 here:
    #0 in realloc asan_malloc_linux.cpp:85
    #1 in Stream_EnsureCapacity (.../libwinpr3.so.3+0x78052)

Full harness source, build notes, and both size variants (128-byte and 256-byte allocations, same signature both times) are attached (pf_channel_drdynvc_oob_read_asan_repro.md).

Impact

Vulnerability class: CWE-125, out-of-bounds heap read.

Who is impacted: operators running the freerdp-proxy component (built by default via WITH_PROXY=ON, but only exercised by operators who actually deploy the proxy role) in a configuration where the backend/target RDP server is not fully trusted — precisely the scenario freerdp-proxy is designed for. Plain FreeRDP clients and plain FreeRDP-based servers (not using the proxy component) are not affected, since the vulnerable code is proxy-specific.

Severity assessment: CVSS 3.1 base score 6.5 (Medium). We note this reflects the standard formula's treatment of network-reachable/no-auth/no-UI bugs; practical impact is more modest than the score alone suggests. The disclosed bytes go to the proxy's own local log rather than back to the attacker, and availability impact is a possible but non-guaranteed crash rather than a reliable DoS.

Methodology Analysis performed using custom CodeQL-based automated sweeps and AI-assisted (Claude/Claude Code) source review and reproduction.

pf_channel_drdynvc_oob_read_asan_repro_20260916_201816.md pf_channel_drdynvc_oob_read_verification_20260916_183851.md

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c<= 3.31.1GitHub