{"schema":"threatline.record/2","record":{"id":"GHSA-g4wg-67xq-8q53","aliases":["GHSA-g4wg-67xq-8q53"],"summary":"Heap-buffer-overflow read in proxy DRDYNVC channel-name handling (DynvcTrackerHandleCreateBack)","details":"### Summary\r\n\r\nA heap-buffer-overflow **read** (CWE-125) exists in FreeRDP's proxy component (`server/proxy/channels/pf_channel_drdynvc.c`,\r\n`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.\r\n\r\n### Details\r\n\r\n```c\r\nstatic PfChannelResult DynvcTrackerHandleCreateBack(ChannelStateTracker* tracker, wStream* s,\r\n                                                    DWORD flags, proxyData* pdata,\r\n                                                    pServerDynamicChannelContext* dynChannel,\r\n                                                    DynChannelContext* dynChannelContext,\r\n                                                    UINT64 dynChannelId)\r\n{\r\n\tproxyChannelDataEventInfo dev = WINPR_C_ARRAY_INIT;\r\n\tconst char* name = Stream_ConstPointer(s);\r\n\tconst size_t nameLen = Stream_GetRemainingLength(s);\r\n\tconst size_t len = strnlen(name, nameLen);\r\n\t...\r\n\tif ((len == 0) || (len == nameLen) || (dynChannelId > UINT16_MAX))\r\n\t{\r\n\t\tchar namebuffer[64] = WINPR_C_ARRAY_INIT;\r\n\t\t(void)_snprintf(namebuffer, sizeof(namebuffer) - 1, \"%s\", name);\r\n\r\n\t\tDynvcTrackerLog(dynChannelContext->log, WLOG_ERROR, dynChannel, cmd, isBackData,\r\n\t\t                \"channel id %\" PRIu64 \", name=%s [...]\",\r\n\t\t                dynChannelId, namebuffer, len, nameLen, ...);\r\n\t\treturn PF_CHANNEL_RESULT_ERROR;\r\n\t}\r\n```\r\n(`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.)\r\n\r\n`name` points into `ChannelStateTracker.currentPacket`, the reassembled dynamic-channel-creation buffer, populated via \r\n`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)`.\r\n\r\n`_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.\r\n\r\n**Reachability, traced through the full call chain:** \r\n- 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`. \r\n- 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. \r\n- 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.\r\n- 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.\r\n\r\nWe 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.\r\n\r\n### PoC\r\n\r\n**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).\r\n\r\n**Wire-level trigger**, sent by the backend server as a single, unfragmented DRDYNVC PDU (`flags = CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST`):\r\n\r\n```\r\nbyte0   = 0x10        // high nibble 0x1 = CREATE_REQUEST_PDU; low 2 bits (cbId) = 00 -> 1-byte channel id\r\nbyte1   = <any value not already an open channel id>\r\nbyte2.. = \"AAAA...A\"  // channel-name bytes, length >= 1, containing NO 0x00 byte anywhere\r\n```\r\n\r\nNo other step in the parser rejects this: the channel-id varint decode succeeds trivially (structural validity only), the \"unknown channel\"\r\nguard is bypassed by design for a fresh `CREATE_REQUEST_PDU`, and `lastPacket` is satisfied by construction (single, complete PDU).\r\n\r\n**Isolated memory-safety confirmation (AddressSanitizer):** we did not stand up a full live two-sided proxy session (real client + real\r\nbackend + 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:\r\n\r\n```\r\n==5692==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x511000000140 ...\r\nREAD of size 267 at 0x511000000140 thread T0\r\n    #0 in printf_common .../sanitizer_common_interceptors_format.inc:563\r\n    #1 in vsnprintf .../sanitizer_common_interceptors.inc:1652\r\n    #2 in snprintf .../sanitizer_common_interceptors.inc:1723\r\n    #3 in run_case harness.c:114\r\n0x511000000140 is located 0 bytes after 256-byte region [0x511000000040,0x511000000140)\r\nallocated by thread T0 here:\r\n    #0 in realloc asan_malloc_linux.cpp:85\r\n    #1 in Stream_EnsureCapacity (.../libwinpr3.so.3+0x78052)\r\n```\r\n\r\nFull harness source, build notes, and both size variants (128-byte and 256-byte allocations, same signature both times) are attached\r\n(`pf_channel_drdynvc_oob_read_asan_repro.md`).\r\n\r\n### Impact\r\n\r\n**Vulnerability class:** CWE-125, out-of-bounds heap read.\r\n\r\n**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.\r\n\r\n**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.\r\n\r\n** Methodology**\r\nAnalysis performed using custom CodeQL-based automated sweeps and AI-assisted (Claude/Claude Code) source review and reproduction.\r\n\r\n[pf_channel_drdynvc_oob_read_asan_repro_20260916_201816.md](https://github.com/user-attachments/files/32319069/pf_channel_drdynvc_oob_read_asan_repro_20260916_201816.md)\r\n[pf_channel_drdynvc_oob_read_verification_20260916_183851.md](https://github.com/user-attachments/files/32319070/pf_channel_drdynvc_oob_read_verification_20260916_183851.md)\r\n","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-g4wg-67xq-8q53","source_modified_at":"2026-09-29T06:37:48Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-g4wg-67xq-8q53","source_modified_at":"2026-09-29T06:37:48Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T06:37:48Z","modified_at":"2026-09-29T06:37:48Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-g4wg-67xq-8q53","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-09-29T06:37:48Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-29T06:37:48Z","content_hash":"8b430e317f3688a9b459f46959b469f63f24e60fc7580bcd9f6ec27beec9c289"}}
