FreeRDP: Heap OOB Read / Information Disclosure in WinPR Clipboard PNG→CF_DIB Synthesis
What fixes it
- 3.32.1FreeRDP, 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
Researcher: Kanishka De Silva (HEXER / HEXERLAB / @HEXER365) Vendor: FreeRDP Project — https://github.com/FreeRDP/FreeRDP Affected component: winpr/libwinpr/utils/image.c, winpr/libwinpr/clipboard/synthetic.c Affected version: master @ 32394214af7965c890d8b43be803d0e34d28f869 (2026-09-26, 3.32.1-dev0) Feature gate: WINPR_UTILS_IMAGE_PNG=ON (default in official Android & iOS clients) Report date: 2026-09-26
---
Runtime Verification Status
CONFIRMED — AddressSanitizer heap-buffer-overflow triggered on the exact upstream commit with a crafted 4 KB PNG payload via the public ClipboardSetData / ClipboardGetData API.
ASAN Report (reproduced locally)
==4548==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f9cf78ef800
READ of size 2048 at 0x7f9cf78ef800 thread T0
#0 __asan_memcpy
#1 Stream_Write winpr/include/winpr/stream.h:1192
#2 winpr_bitmap_write_buffer winpr/libwinpr/utils/image.c:339 <- OOB READ
#3 winpr_image_write_buffer winpr/libwinpr/utils/image.c:1387
#4 clipboard_synthesize_image_format_to_bmp winpr/libwinpr/clipboard/synthetic.c:565
#5 clipboard_synthesize_image_png_to_bmp winpr/libwinpr/clipboard/synthetic.c:588
#6 ClipboardGetData winpr/libwinpr/clipboard/clipboard.c:483
#7 main (PoC)
0x7f9cf78ef800 is located 0 bytes after 524288-byte region [...]
allocated by thread T0 here:
#1 winpr_read_png_from_buffer winpr/libwinpr/utils/image.c:1106 <- undersized malloc
#3 winpr_image_read_buffer winpr/libwinpr/utils/image.c:664Plain-build OOB Measurement
- CF_DIB returned: 4,194,358 bytes
- Legitimate pixel content: 524,288 bytes (2048×2048 ÷ 8)
- Out-of-bounds bytes delivered to clipboard: 3,670,070 (~3.5 MiB)
- Non-zero OOB bytes (confirmed heap contents, not unmapped): 3,661,884
---
Root Cause
For PNG inputs with bit_depth < 8 (1/2/4-bit grayscale or palette), winpr_read_png_from_buffer() allocates using the bit-packed footprint:
// image.c:1101-1119 const size_t stride = 1ULL * width * bpp / 8ull; // e.g. 2048×1/8 = 256 bytes/row const size_t size = 1ULL * width * height * bpp / 8ull; // PACKED total rc = malloc(size); // UNDERSIZED: only width*height/8 bytes
Every downstream consumer, including winpr_image_read_buffer(), derives stride assuming one byte per pixel for sub-8-bpp images:
// image.c:665-668 image->bytesPerPixel = (image->bitsPerPixel + 7) / 8; // = 1 for bpp=1 image->scanline = image->width * image->bytesPerPixel; // = width (not width/8)
winpr_bitmap_write_buffer() then copies full stride-byte rows (where stride = width) from the undersized allocation:
// image.c:335-341
for (size_t y = 0; y < height; y++) {
const BYTE* line = &data[stride * y]; // stride = width (unpacked assumption)
Stream_Write(s, line, stride); // OOB READ once y >= height*bpp/8
}The 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.
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).
---
Attack Vector and Data Flow
Malicious RDP server
└─ CLIPRDR FormatDataResponse "image/png"
payload: crafted 1-bpp PNG (~4 KB wire size)
└─ ClipboardSetData("image/png", ...)
└─ Victim pastes into local app (or always-on clipboard monitor)
└─ ClipboardGetData(CF_DIB)
└─ clipboard_synthesize_image_png_to_bmp (synthetic.c:588)
└─ winpr_image_read_buffer malloc(w*h/8) <- undersized
└─ winpr_bitmap_write_buffer READ w*h bytes <- OOB
=> ~3.5 MiB adjacent heap memory delivered as CF_DIB
=> same OOB bytes base64-encoded into HTML clipboard flavorImpact: Adjacent process heap memory is disclosed to whichever application the user pastes into. The attacker controls the trigger timing and PNG dimensions.
---
Affected Configurations
WINPR_UTILS_IMAGE_PNG=ON(libpng decode path). **WITH_LODEPNGis NOT affected** — it always expands to 32 bpp.- Official FreeRDP Android client: builds with this option ON by default (
client/Android/cmake/ExternalDeps.cmake:15). iOS external build forwards the same option. - Distro Linux packages: typically not affected (option usually OFF in package builds).
- Introduced in WinPR 3.3.0 with the
WINPR_UTILS_IMAGE_*option set; still vulnerable at master3239421(2026-09-26) inclusive of partial hardening from53b98fe(contained in 3.32.0).
---
Reproduction Steps
git clone https://github.com/FreeRDP/FreeRDP.git && cd FreeRDP
git checkout 32394214af7965c890d8b43be803d0e34d28f869
# Build WinPR with ASAN, PNG support, no LTO
cmake -B build-asan -DBUILD_SHARED_LIBS=OFF -DWITH_SERVER=OFF \
-DWITH_CLIENT=OFF -DWINPR_UTILS_IMAGE_PNG=ON \
-DWITH_X11=OFF -DWITH_WAYLAND=OFF \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_C_FLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build-asan -- -j$(nproc)
# Generate crafted payload (2048×2048, 1-bpp grayscale PNG, ~4 KB)
python3 gen_1bpp_png.py /tmp/poc_1bpp.png # included with this advisory
# Compile PoC driver
clang -fsanitize=address -g -O1 -fno-omit-frame-pointer \
-I build-asan/winpr/include -I winpr/include \
PocClipboardPng.c \
-Wl,--whole-archive build-asan/winpr/libwinpr/libwinpr3.a -Wl,--no-whole-archive \
-lpng -lssl -lcrypto -lpthread -ldl -lm -lz \
-lkrb5 -lk5crypto -lkrb5support -lcom_err -licui18n -licuuc -licudata \
-o PocClipboardPng
# Trigger
ASAN_OPTIONS=detect_leaks=0 ./PocClipboardPng /tmp/poc_1bpp.png
# Expected: heap-buffer-overflow at image.c:339 / alloc at image.c:1106---
Suggested Fix
The 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:
- Allocate
height × png_get_rowbytes()and propagate the packed stride consistently through all consumers, or - Reject
bit_depth < 8inwinpr_read_png_from_buffer()(functional regression, but minimal change).
Additionally, activate the size parameter in winpr_bitmap_write_buffer() to bound row reads, and reject malloc(0) returns in the decode path.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c<=3.32.0GitHub