FreeRDP: WinPR clipboard: server-controlled `FILEDESCRIPTORW.cFileName` is scanned by `_wcschr()` without a NUL-termination guarantee — heap out-of-bounds read in a connecting client
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
This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics.
Summary
FreeRDP's WinPR clipboard converts a Windows FileGroupDescriptorW blob received over the CLIPRDR virtual channel into a POSIX text/uri-list (and the GNOME/MATE copied-files targets). A malicious or compromised RDP server can answer a CB_FORMAT_DATA_RESPONSE with an odd-length FILEDESCRIPTORW blob whose cFileName contains no NUL WCHAR. ClipboardSetData() copies the wire bytes into a calloc(size + sizeof(WCHAR)) buffer whose two-byte zero pad only forms an aligned NUL WCHAR when size is even; an odd size leaves the buffer with no terminating WCHAR on the grid cFileName sits on. convert_filedescriptors_to_file_list() then runs the unbounded scans _wcschr(dsc->cFileName, L'\\') and count_special_chars(dsc->cFileName) over that server-controlled field, reading past the end of the heap allocation.
The affected clients are the Unix desktop builds — X11, Wayland and SDL — where this synthesizer is the paste path and WITH_FUSE is on by default; the Windows client uses the native clipboard and does not reach this code. The trigger is one clipboard paste of server-copied files into a local application that requests text/uri-list. The demonstrated effect is a client-side heap out-of-bounds read (crash / denial of service).
Affected component and version
winpr/libwinpr/clipboard/synthetic_file.c, function convert_filedescriptors_to_file_list(), line 758 (and the identical unbounded scan at line 763), reached from convert_filedescriptors_to_uri_list() / ..._gnome_copied_files() / ..._mate_copied_files().
Verified present and reproduced at current master 43e8453d121709811051522d1d383b265c0bb63c (2026-09-14; the vulnerable lines are unchanged from a0114ebd), and present in the released tag 3.31.1, so this is not development-branch-only.
Reachability requires WITH_FUSE (default ON on Linux/BSD, client/common/CMakeLists.txt:48), because clipboard->delegate.basePath is only populated on that path (client/common/client_cliprdr_file.c:2009); with WITH_FUSE=OFF the function returns early at synthetic_file.c:730.
Root cause
ClipboardSetData() copies the CLIPRDR FORMAT_DATA_RESPONSE payload into calloc(size + sizeof(WCHAR), 1) (winpr/libwinpr/clipboard/clipboard.c:509), relying on the two appended zero bytes to terminate any trailing string. When the wire length size is odd, those two zero bytes do not lie on the WCHAR grid that the descriptor's cFileName field occupies, so the buffer contains no aligned NUL WCHAR at its end.
convert_filedescriptors_to_file_list() accepts the blob whenever count = (size - 4) / sizeof(FILEDESCRIPTORW) equals the wire nrDescriptors (synthetic_file.c:739-742), which permits up to 591 bytes of trailing slack and therefore any parity of size. It then calls _wcschr(dsc->cFileName, L'\\') and count_special_chars(dsc->cFileName) — both unbounded scans — on a 260-WCHAR field the RDP server fully controls and which nothing in the receive path requires to contain a NUL WCHAR.
Concretely, for a 597-byte payload holding one descriptor whose cFileName is filled with 0x4141: count = 1, calloc(599) gives valid indices 0..598, cFileName starts at byte 76, and the scan reads WCHARs at 76, 78, ... 596 ({0x41,0x00}, non-zero) and then at 598, whose second byte is one past the end of the 599-byte region. The scan continues over adjacent heap until a NUL or L'\\' WCHAR is encountered.
CWE-170 (improper null termination) leading to CWE-125 (out-of-bounds read).
Attack path and primitive
- A malicious or compromised RDP server advertises
FileGroupDescriptorWinCB_FORMAT_LIST. - The user pastes the clipboard files into a local application that asks for
text/uri-list
(or the GNOME/MATE copied-files targets); the client sends CB_FORMAT_DATA_REQUEST.
- The server answers
CB_FORMAT_DATA_RESPONSEwith the crafted odd-length blob. client/X11/xf_cliprdr.c:2479(equivalentlyclient/Wayland/wlf_cliprdr.c:908,
client/SDL/SDL3/sdl_clip.cpp:878) calls ClipboardSetData() with the wire length verbatim, then ClipboardGetData() for the target format, invoking the synthesizer.
Primitive: a read-only scan, 2 bytes at a time, past the end of the heap allocation, terminating only when adjacent heap content happens to hold a NUL or L'\\' WCHAR. The over-read bytes are not copied into the produced URI list (ConvertWCharNToUtf8 is bounded by ARRAYSIZE(dsc->cFileName)), so this is not a direct information disclosure; the observable outcomes are a client crash when the scan reaches an unmapped page, and an unbounded contribution to the subsequent alloc computation at synthetic_file.c:760-764.
Evidence
Validated run at current master 43e8453d121709811051522d1d383b265c0bb63c, WinPR built from source with AddressSanitizer (clang-20, -g -fno-omit-frame-pointer -O1 -fsanitize=address):
[*] payload_size = 597 (odd)
[*] FILEDESCRIPTORW size = 592, cFileName offset = 72
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x716a213e08d6 ...
READ of size 2 at 0x716a213e08d6 thread T0
#0 _wcschr /src/repo/winpr/libwinpr/crt/string.c:453:9
#1 convert_filedescriptors_to_file_list /src/repo/winpr/libwinpr/clipboard/synthetic_file.c:758:7
#2 convert_filedescriptors_to_uri_list /src/repo/winpr/libwinpr/clipboard/synthetic_file.c:876:9
#3 ClipboardGetData /src/repo/winpr/libwinpr/clipboard/clipboard.c:483:14
0x716a213e08d7 is located 0 bytes after 599-byte region [0x716a213e0680,0x716a213e08d7)
allocated by thread T0 here:
#1 ClipboardSetData /src/repo/winpr/libwinpr/clipboard/clipboard.c:509:20Reproduction
unzip ANT-2026-JBQWJCSR-e2e.zip && cd ANT-2026-JBQWJCSR docker build -t ant-jbqwjcsr docker-e2e/ docker run --rm ant-jbqwjcsr
No special docker run flags are needed. The image builds FreeRDP from source at ARG TARGET_COMMIT (default 43e8453d121709811051522d1d383b265c0bb63c, current master; override with --build-arg TARGET_COMMIT=<sha> to test other commits) and runs a single trigger program that makes only the public WinPR calls the clients make: ClipboardCreate(), set delegate->basePath (as client_cliprdr_file.c:2009 does), ClipboardSetData(FileGroupDescriptorW, <597-byte blob>), ClipboardGetData(text/uri-list). Nothing upstream is patched, stubbed or reimplemented.
Scope caveat, stated plainly: the trigger is driven through the WinPR public API rather than through a rogue RDP server plus a live paste, so the wire-level end-to-end has not been exercised in the container; the API arguments are however taken verbatim from the client code path cited above.
Reproducer archive (attached)
Reproducer attached as ANT-2026-JBQWJCSR-e2e.zip (contains docker-e2e/ and a README.txt with the commands above).
Credit
Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics. Handled under Anthropic's coordinated vulnerability disclosure policy (https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Please credit Claude and Ada Logics in any advisory.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c<= 3.31.1GitHub