{"schema":"threatline.record/2","record":{"id":"GHSA-f3vg-h45x-6wgf","aliases":["GHSA-f3vg-h45x-6wgf"],"summary":" Heap OOB Read / Information Disclosure in WinPR Clipboard PNG→CF_DIB Synthesis","details":"**Researcher:** Kanishka De Silva ([HEXER](https://t.me/H3X4R) / HEXERLAB / @HEXER365)  \r\n**Vendor:** FreeRDP Project — https://github.com/FreeRDP/FreeRDP  \r\n**Affected component:** `winpr/libwinpr/utils/image.c`, `winpr/libwinpr/clipboard/synthetic.c`  \r\n**Affected version:** master @ `32394214af7965c890d8b43be803d0e34d28f869` (2026-09-26, 3.32.1-dev0)  \r\n**Feature gate:** `WINPR_UTILS_IMAGE_PNG=ON` (default in official Android & iOS clients)  \r\n**Report date:** 2026-09-26  \r\n  \r\n\r\n \r\n---\r\n \r\n## Runtime Verification Status\r\n \r\n**CONFIRMED** — AddressSanitizer heap-buffer-overflow triggered on the exact upstream commit with a crafted 4 KB PNG payload via the public `ClipboardSetData` / `ClipboardGetData` API.\r\n \r\n### ASAN Report (reproduced locally)\r\n \r\n```\r\n==4548==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f9cf78ef800\r\nREAD of size 2048 at 0x7f9cf78ef800 thread T0\r\n    #0  __asan_memcpy\r\n    #1  Stream_Write           winpr/include/winpr/stream.h:1192\r\n    #2  winpr_bitmap_write_buffer  winpr/libwinpr/utils/image.c:339   <- OOB READ\r\n    #3  winpr_image_write_buffer   winpr/libwinpr/utils/image.c:1387\r\n    #4  clipboard_synthesize_image_format_to_bmp  winpr/libwinpr/clipboard/synthetic.c:565\r\n    #5  clipboard_synthesize_image_png_to_bmp     winpr/libwinpr/clipboard/synthetic.c:588\r\n    #6  ClipboardGetData       winpr/libwinpr/clipboard/clipboard.c:483\r\n    #7  main (PoC)\r\n \r\n0x7f9cf78ef800 is located 0 bytes after 524288-byte region [...]\r\nallocated by thread T0 here:\r\n    #1  winpr_read_png_from_buffer  winpr/libwinpr/utils/image.c:1106  <- undersized malloc\r\n    #3  winpr_image_read_buffer     winpr/libwinpr/utils/image.c:664\r\n```\r\n \r\n### Plain-build OOB Measurement\r\n \r\n- CF_DIB returned: **4,194,358 bytes**\r\n- Legitimate pixel content: 524,288 bytes (2048×2048 ÷ 8)\r\n- **Out-of-bounds bytes delivered to clipboard: 3,670,070 (~3.5 MiB)**\r\n- Non-zero OOB bytes (confirmed heap contents, not unmapped): 3,661,884\r\n---\r\n \r\n## Root Cause\r\n \r\nFor PNG inputs with `bit_depth < 8` (1/2/4-bit grayscale or palette), `winpr_read_png_from_buffer()` allocates using the **bit-packed** footprint:\r\n \r\n```c\r\n// image.c:1101-1119\r\nconst size_t stride = 1ULL * width * bpp / 8ull;   // e.g. 2048×1/8 = 256 bytes/row\r\nconst size_t size   = 1ULL * width * height * bpp / 8ull;  // PACKED total\r\nrc = malloc(size);  // UNDERSIZED: only width*height/8 bytes\r\n```\r\n \r\nEvery downstream consumer, including `winpr_image_read_buffer()`, derives stride assuming **one byte per pixel** for sub-8-bpp images:\r\n \r\n```c\r\n// image.c:665-668\r\nimage->bytesPerPixel = (image->bitsPerPixel + 7) / 8;  // = 1 for bpp=1\r\nimage->scanline = image->width * image->bytesPerPixel;  // = width (not width/8)\r\n```\r\n \r\n`winpr_bitmap_write_buffer()` then copies full `stride`-byte rows (where `stride = width`) from the undersized allocation:\r\n \r\n```c\r\n// image.c:335-341\r\nfor (size_t y = 0; y < height; y++) {\r\n    const BYTE* line = &data[stride * y];  // stride = width (unpacked assumption)\r\n    Stream_Write(s, line, stride);         // OOB READ once y >= height*bpp/8\r\n}\r\n```\r\n \r\nThe `size` parameter is `WINPR_ATTR_UNUSED` and provides no bounds check. The partial hardening in upstream commit `53b98fe` does not close the hole because for a 1-bpp image both `bpp_stride` and `stride` evaluate to `width`, so the guard is never triggered.\r\n \r\n**Additional variant:** `width*bpp < 8` yields `malloc(0)`, and re-encoding fills the returned buffer (or any allocation the allocator returns) entirely with uninitialized heap bytes (CWE-908).\r\n \r\n---\r\n \r\n## Attack Vector and Data Flow\r\n \r\n```\r\nMalicious RDP server\r\n  └─ CLIPRDR FormatDataResponse  \"image/png\"\r\n     payload: crafted 1-bpp PNG (~4 KB wire size)\r\n  └─ ClipboardSetData(\"image/png\", ...)\r\n  └─ Victim pastes into local app (or always-on clipboard monitor)\r\n      └─ ClipboardGetData(CF_DIB)\r\n          └─ clipboard_synthesize_image_png_to_bmp  (synthetic.c:588)\r\n              └─ winpr_image_read_buffer             malloc(w*h/8)  <- undersized\r\n              └─ winpr_bitmap_write_buffer           READ w*h bytes  <- OOB\r\n                 => ~3.5 MiB adjacent heap memory delivered as CF_DIB\r\n                 => same OOB bytes base64-encoded into HTML clipboard flavor\r\n```\r\n \r\n**Impact:** Adjacent process heap memory is disclosed to whichever application the user pastes into. The attacker controls the trigger timing and PNG dimensions.\r\n \r\n---\r\n \r\n## Affected Configurations\r\n \r\n- `WINPR_UTILS_IMAGE_PNG=ON` (libpng decode path). **`WITH_LODEPNG` is NOT affected** — it always expands to 32 bpp.\r\n- **Official FreeRDP Android client:** builds with this option ON by default (`client/Android/cmake/ExternalDeps.cmake:15`). iOS external build forwards the same option.\r\n- Distro Linux packages: typically not affected (option usually OFF in package builds).\r\n- Introduced in WinPR 3.3.0 with the `WINPR_UTILS_IMAGE_*` option set; still vulnerable at master `3239421` (2026-09-26) inclusive of partial hardening from `53b98fe` (contained in 3.32.0).\r\n---\r\n \r\n## Reproduction Steps\r\n \r\n```bash\r\ngit clone https://github.com/FreeRDP/FreeRDP.git && cd FreeRDP\r\ngit checkout 32394214af7965c890d8b43be803d0e34d28f869\r\n \r\n# Build WinPR with ASAN, PNG support, no LTO\r\ncmake -B build-asan -DBUILD_SHARED_LIBS=OFF -DWITH_SERVER=OFF \\\r\n      -DWITH_CLIENT=OFF -DWINPR_UTILS_IMAGE_PNG=ON \\\r\n      -DWITH_X11=OFF -DWITH_WAYLAND=OFF \\\r\n      -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \\\r\n      -DCMAKE_C_COMPILER=clang \\\r\n      -DCMAKE_C_FLAGS=\"-fsanitize=address -g -O1 -fno-omit-frame-pointer\" \\\r\n      -DCMAKE_EXE_LINKER_FLAGS=\"-fsanitize=address\"\r\ncmake --build build-asan -- -j$(nproc)\r\n \r\n# Generate crafted payload (2048×2048, 1-bpp grayscale PNG, ~4 KB)\r\npython3 gen_1bpp_png.py /tmp/poc_1bpp.png   # included with this advisory\r\n \r\n# Compile PoC driver\r\nclang -fsanitize=address -g -O1 -fno-omit-frame-pointer \\\r\n  -I build-asan/winpr/include -I winpr/include \\\r\n  PocClipboardPng.c \\\r\n  -Wl,--whole-archive build-asan/winpr/libwinpr/libwinpr3.a -Wl,--no-whole-archive \\\r\n  -lpng -lssl -lcrypto -lpthread -ldl -lm -lz \\\r\n  -lkrb5 -lk5crypto -lkrb5support -lcom_err -licui18n -licuuc -licudata \\\r\n  -o PocClipboardPng\r\n \r\n# Trigger\r\nASAN_OPTIONS=detect_leaks=0 ./PocClipboardPng /tmp/poc_1bpp.png\r\n# Expected: heap-buffer-overflow at image.c:339 / alloc at image.c:1106\r\n```\r\n \r\n---\r\n \r\n## Suggested Fix\r\n \r\nThe most robust fix: expand sub-8-bpp images to one byte per pixel during decode (PNG `PNG_TRANSFORM_EXPAND`), keeping the `wImage` invariant intact. Alternatively:\r\n \r\n- Allocate `height × png_get_rowbytes()` and propagate the packed stride consistently through all consumers, or\r\n- Reject `bit_depth < 8` in `winpr_read_png_from_buffer()` (functional regression, but minimal change).\r\nAdditionally, activate the `size` parameter in `winpr_bitmap_write_buffer()` to bound row reads, and reject `malloc(0)` returns in the decode path.\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-f3vg-h45x-6wgf","source_modified_at":"2026-10-05T20:32:24Z","fetched_at":"2026-10-23T20:39:26.410340Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<=3.32.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-f3vg-h45x-6wgf","source_modified_at":"2026-10-05T20:32:24Z","fetched_at":"2026-10-23T20:39:26.410340Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.1"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-05T20:32:24Z","modified_at":"2026-10-05T20:32:24Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-f3vg-h45x-6wgf","fetched_at":"2026-10-23T20:39:26.410340Z","source_modified_at":"2026-10-05T20:32:24Z"}],"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-10-05T20:32:24Z","content_hash":"e7dad1f5c5192790fbe33f351d817d6d698b10699839e28dae18a719bcf78867"}}
