FreeRDP: Heap-buffer-overflow read in proxy DRDYNVC channel-name handling (DynvcTrackerHandleCreateBack)
What fixes it
- 3.32.0FreeRDP, cGitHub
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 owngetDirection()). -DynvcTrackerHandleCreaterequireslastPacket == TRUEbefore dispatching here, soname/nameLenreflect a complete, fully reassembledCREATE_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
lastPacketgate) inspects the channel-name bytes for a NUL or bounds their length before thestrnlencall shown above. - The read is unconditional: it happens the instant the
ifbranch is entered, before any log-level check, it does not depend on the resultingWLOG_ERRORmessage 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