{"schema":"threatline.record/2","record":{"id":"GHSA-jp2r-gm4v-wvq2","aliases":["GHSA-jp2r-gm4v-wvq2"],"summary":"WinPR clipboard: server-controlled `FILEDESCRIPTORW.cFileName` is scanned by `_wcschr()` without a NUL-termination guarantee — heap out-of-bounds read in a connecting client","details":"This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by\r\nme from Ada Logics.\r\n\r\n## Summary\r\n\r\nFreeRDP's WinPR clipboard converts a Windows `FileGroupDescriptorW` blob received over the CLIPRDR\r\nvirtual channel into a POSIX `text/uri-list` (and the GNOME/MATE copied-files targets). A malicious\r\nor compromised RDP server can answer a `CB_FORMAT_DATA_RESPONSE` with an odd-length `FILEDESCRIPTORW`\r\nblob whose `cFileName` contains no NUL WCHAR. `ClipboardSetData()` copies the wire bytes into a\r\n`calloc(size + sizeof(WCHAR))` buffer whose two-byte zero pad only forms an aligned NUL WCHAR when\r\n`size` is even; an odd `size` leaves the buffer with no terminating WCHAR on the grid `cFileName`\r\nsits on. `convert_filedescriptors_to_file_list()` then runs the unbounded scans\r\n`_wcschr(dsc->cFileName, L'\\\\')` and `count_special_chars(dsc->cFileName)` over that\r\nserver-controlled field, reading past the end of the heap allocation.\r\n\r\nThe affected clients are the Unix desktop builds — X11, Wayland and SDL — where this synthesizer is\r\nthe paste path and `WITH_FUSE` is on by default; the Windows client uses the native clipboard and\r\ndoes not reach this code. The trigger is one clipboard paste of server-copied files into a local\r\napplication that requests `text/uri-list`. The demonstrated effect is a client-side heap\r\nout-of-bounds read (crash / denial of service).\r\n\r\n## Affected component and version\r\n\r\n`winpr/libwinpr/clipboard/synthetic_file.c`, function `convert_filedescriptors_to_file_list()`,\r\nline 758 (and the identical unbounded scan at line 763), reached from\r\n`convert_filedescriptors_to_uri_list()` / `..._gnome_copied_files()` / `..._mate_copied_files()`.\r\n\r\nVerified present and reproduced at current master `43e8453d121709811051522d1d383b265c0bb63c`\r\n(2026-09-14; the vulnerable lines are unchanged from `a0114ebd`), and present in the released tag\r\n`3.31.1`, so this is not development-branch-only.\r\n\r\nReachability requires `WITH_FUSE` (default `ON` on Linux/BSD, `client/common/CMakeLists.txt:48`),\r\nbecause `clipboard->delegate.basePath` is only populated on that path\r\n(`client/common/client_cliprdr_file.c:2009`); with `WITH_FUSE=OFF` the function returns early at\r\n`synthetic_file.c:730`.\r\n\r\n## Root cause\r\n\r\n`ClipboardSetData()` copies the CLIPRDR `FORMAT_DATA_RESPONSE` payload into\r\n`calloc(size + sizeof(WCHAR), 1)` (`winpr/libwinpr/clipboard/clipboard.c:509`), relying on the two\r\nappended zero bytes to terminate any trailing string. When the wire length `size` is odd, those two\r\nzero bytes do not lie on the WCHAR grid that the descriptor's `cFileName` field occupies, so the\r\nbuffer contains no aligned NUL WCHAR at its end.\r\n\r\n`convert_filedescriptors_to_file_list()` accepts the blob whenever\r\n`count = (size - 4) / sizeof(FILEDESCRIPTORW)` equals the wire `nrDescriptors`\r\n(`synthetic_file.c:739-742`), which permits up to 591 bytes of trailing slack and therefore any parity\r\nof `size`. It then calls `_wcschr(dsc->cFileName, L'\\\\')` and `count_special_chars(dsc->cFileName)` —\r\nboth unbounded scans — on a 260-WCHAR field the RDP server fully controls and which nothing in the\r\nreceive path requires to contain a NUL WCHAR.\r\n\r\nConcretely, for a 597-byte payload holding one descriptor whose `cFileName` is filled with `0x4141`:\r\n`count = 1`, `calloc(599)` gives valid indices 0..598, `cFileName` starts at byte 76, and the scan\r\nreads WCHARs at 76, 78, ... 596 (`{0x41,0x00}`, non-zero) and then at 598, whose second byte is one\r\npast the end of the 599-byte region. The scan continues over adjacent heap until a NUL or `L'\\\\'`\r\nWCHAR is encountered.\r\n\r\nCWE-170 (improper null termination) leading to CWE-125 (out-of-bounds read).\r\n\r\n## Attack path and primitive\r\n\r\n1. A malicious or compromised RDP server advertises `FileGroupDescriptorW` in `CB_FORMAT_LIST`.\r\n2. The user pastes the clipboard files into a local application that asks for `text/uri-list`\r\n   (or the GNOME/MATE copied-files targets); the client sends `CB_FORMAT_DATA_REQUEST`.\r\n3. The server answers `CB_FORMAT_DATA_RESPONSE` with the crafted odd-length blob.\r\n4. `client/X11/xf_cliprdr.c:2479` (equivalently `client/Wayland/wlf_cliprdr.c:908`,\r\n   `client/SDL/SDL3/sdl_clip.cpp:878`) calls `ClipboardSetData()` with the wire length verbatim, then\r\n   `ClipboardGetData()` for the target format, invoking the synthesizer.\r\n\r\nPrimitive: a read-only scan, 2 bytes at a time, past the end of the heap allocation, terminating only\r\nwhen adjacent heap content happens to hold a NUL or `L'\\\\'` WCHAR. The over-read bytes are not copied\r\ninto the produced URI list (`ConvertWCharNToUtf8` is bounded by `ARRAYSIZE(dsc->cFileName)`), so this\r\nis not a direct information disclosure; the observable outcomes are a client crash when the scan\r\nreaches an unmapped page, and an unbounded contribution to the subsequent `alloc` computation at\r\n`synthetic_file.c:760-764`.\r\n\r\n## Evidence\r\n\r\nValidated run at current master `43e8453d121709811051522d1d383b265c0bb63c`, WinPR built from source\r\nwith AddressSanitizer (clang-20, `-g -fno-omit-frame-pointer -O1 -fsanitize=address`):\r\n\r\n```\r\n[*] payload_size = 597 (odd)\r\n[*] FILEDESCRIPTORW size = 592, cFileName offset = 72\r\n==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x716a213e08d6 ...\r\nREAD of size 2 at 0x716a213e08d6 thread T0\r\n    #0 _wcschr /src/repo/winpr/libwinpr/crt/string.c:453:9\r\n    #1 convert_filedescriptors_to_file_list /src/repo/winpr/libwinpr/clipboard/synthetic_file.c:758:7\r\n    #2 convert_filedescriptors_to_uri_list /src/repo/winpr/libwinpr/clipboard/synthetic_file.c:876:9\r\n    #3 ClipboardGetData /src/repo/winpr/libwinpr/clipboard/clipboard.c:483:14\r\n\r\n0x716a213e08d7 is located 0 bytes after 599-byte region [0x716a213e0680,0x716a213e08d7)\r\nallocated by thread T0 here:\r\n    #1 ClipboardSetData /src/repo/winpr/libwinpr/clipboard/clipboard.c:509:20\r\n```\r\n\r\n## Reproduction\r\n\r\n```\r\nunzip ANT-2026-JBQWJCSR-e2e.zip && cd ANT-2026-JBQWJCSR\r\ndocker build -t ant-jbqwjcsr docker-e2e/\r\ndocker run --rm ant-jbqwjcsr\r\n```\r\n\r\nNo special `docker run` flags are needed. The image builds FreeRDP from source at\r\n`ARG TARGET_COMMIT` (default `43e8453d121709811051522d1d383b265c0bb63c`, current master; override with\r\n`--build-arg TARGET_COMMIT=<sha>` to test other commits) and runs a single trigger program that makes\r\nonly the public WinPR calls the clients make: `ClipboardCreate()`, set `delegate->basePath` (as\r\n`client_cliprdr_file.c:2009` does), `ClipboardSetData(FileGroupDescriptorW, <597-byte blob>)`,\r\n`ClipboardGetData(text/uri-list)`. Nothing upstream is patched, stubbed or reimplemented.\r\n\r\nScope caveat, stated plainly: the trigger is driven through the WinPR public API rather than through a\r\nrogue RDP server plus a live paste, so the wire-level end-to-end has not been exercised in the\r\ncontainer; the API arguments are however taken verbatim from the client code path cited above.\r\n\r\n## Reproducer archive (attached)\r\n\r\nReproducer attached as `ANT-2026-JBQWJCSR-e2e.zip` (contains `docker-e2e/` and a `README.txt` with the\r\ncommands above).\r\n\r\n## Credit\r\n\r\nFound by Anthropic using Claude to study the security of open-source software, validated manually by\r\nAda Logics. Handled under Anthropic's coordinated vulnerability disclosure policy\r\n(https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Please credit Claude and\r\nAda Logics in any advisory.\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-jp2r-gm4v-wvq2","source_modified_at":"2026-09-28T08:05:49Z","fetched_at":"2026-10-26T12:48:13.248886Z","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-jp2r-gm4v-wvq2","source_modified_at":"2026-09-28T08:05:49Z","fetched_at":"2026-10-26T12:48:13.248886Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":null,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T08:05:49Z","modified_at":"2026-09-28T08:05:49Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jp2r-gm4v-wvq2","fetched_at":"2026-10-26T12:48:13.248886Z","source_modified_at":"2026-09-28T08:05:49Z"}],"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-28T08:05:49Z","content_hash":"b73c40e47997591d75cafcaf1bd6c4ce1ba84f2bceca5591247328240460a892"}}
