Automated Threat Modeling

Brief Summary: FreeRDP Proxy CVE-2026-121952, an Out-of-Bounds Write in Bitmap Cache v2 Capability Handling (with PoC, Patch, and Detection Analysis)

A brief summary of CVE-2026-121952, an out-of-bounds write in the FreeRDP proxy triggered when a connecting client declares fewer than five bitmap cache v2 cells. Includes the maintainer PoC, patch commit analysis, and crash-based and protocol-level detection notes.

CVE Analysis

12 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-04

Brief Summary: FreeRDP Proxy CVE-2026-121952, an Out-of-Bounds Write in Bitmap Cache v2 Capability Handling (with PoC, Patch, and Detection Analysis)
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A connecting client can corrupt or leak heap memory inside a FreeRDP proxy simply by declaring the wrong number of bitmap cache cells during capability negotiation. In deployments that place FreeRDP's proxy between untrusted clients and backend RDP servers, a peer that only needs to reach the Confirm Active stage can trigger a four-byte out-of-bounds write, disclosure of adjacent proxy heap data to the backend, or a crash.

FreeRDP is a free, Apache-licensed implementation of the Remote Desktop Protocol, shipping both a library and client and server components. It is widely integrated downstream: the project history describes use in RDP proxies and other open source and commercial products, and Check Point's research documents Apache Guacamole using FreeRDP as its RDP client. That reach is what makes a memory safety defect in the proxy component worth examining carefully, even though the available evidence does not establish that every downstream product is affected by this specific issue.

Technical information

The vulnerability is classified as CWE-787, an out-of-bounds write. The core defect is a mismatch between an attacker-influenced allocation length and consumers that access fixed array indices.

The parser rdp_read_bitmap_cache_v2_capability_set, in libfreerdp/core/capabilities.c, rejects numCellCaches only when the value exceeds 5, so it accepts values 0 through 4. Downstream, rdp_apply_bitmap_cache_v2_capability_set reallocates BitmapCacheV2CellInfo to exactly the declared count, which leaves the pointer NULL when the count is 0.

The 3.32.1 writer shows the assumption that five entries always exist. It stores into fixed indices and serializes index 4 directly:

settings->BitmapCacheV2CellInfo[0].persistent = 1; ... settings->BitmapCacheV2CellInfo[4].persistent = 1; ... rdp_write_bitmap_cache_cell_info( s, &settings->BitmapCacheV2CellInfo[4]); /* bitmapCache4CellInfo (4 bytes) */

The same fixed-index assumption appears in rdp_send_client_persistent_key_list_pdu in libfreerdp/core/activation.c, which also reads index 4. Validating only the maximum permitted count does not establish the minimum storage these consumers require.

In the proxy, the shortened array from the untrusted downstream client crosses a trust boundary. rdp_recv_confirm_active receives the client's capabilities, pf_server_post_connect copies the settings into the upstream client context used to reach the backend, and freerdp_settings_int_buffer_copy preserves the shortened length. The proxy then writes its own Confirm Active to the backend, and the fixed-index consumers run against a short allocation.

Attack flow

  1. The attacker connects to the proxy's listening port and completes the X.224, MCS, and GCC stages far enough to send a Confirm Active PDU. Where the proxy configuration requires authentication, the peer must authenticate first, but the advisory still treats that authenticated peer as untrusted.
  2. In the Confirm Active, the client's BITMAP_CACHE_V2 capability set declares numCellCaches as a value from 0 to 4.
  3. The parser accepts the value, and the array is reallocated to the shortened count (or left NULL at count 0).
  4. The proxy copies the shortened settings into its upstream client context and later serializes its own Confirm Active to the backend using fixed indices 0 through 4.
  5. Depending on the declared count and whether persistence is enabled, this produces a four-byte heap out-of-bounds write, serialization of adjacent heap bytes into the PDU sent to the backend, or a NULL dereference crash.

The identified recipient of the disclosed data is the backend, so the evidence does not establish that every connecting client directly receives leaked contents. No successful remote code execution is reported for this CVE. Memory corruption alone does not establish an achieved code execution chain.

Proof of concept

A complete, maintainer-published proof of concept exists in the FreeRDP GitHub Security Advisory GHSA-xc85-4mqc-xgcx, published by akallabeth on October 20, 2026, affecting FreeRDP versions through 3.32.1 and patched in 3.32.2. NVD and VulnCheck map this GHSA to CVE-2026-121952 even though the advisory page itself still displays "No known CVE."

The PoC has three components: a malicious client (malclient.c), a minimal headless upstream backend server (backend.c), and a proxy configuration (proxy.ini) with front-end 127.0.0.1:39100 and backend target 127.0.0.1:39101. It was verified against commit 651ab269524adbda4d673efa60d2e391ef7555a7.

Build:

git checkout 651ab269524adbda4d673efa60d2e391ef7555a7 cmake -S . -B build -DWITH_SERVER=ON -DWITH_PROXY=ON cmake --build build

Run:

./build/backend 39101 cert.pem key.pem ./build/freerdp-proxy proxy.ini ./build/malclient 127.0.0.1 39100 0

The malicious client is a stock FreeRDP client core that overrides the bitmap-cache capability via freerdp_settings_set_uint32(context->settings, FreeRDP_BitmapCacheV2NumCells, numCells) and optionally sets FreeRDP_BitmapCachePersistEnabled, which turns the fixed-index array access into an out-of-bounds write through the persistent-key-list path. The backend is just enough of a FreeRDP server to accept the proxy's outbound client connection and drive the state machine to emit Demand Active. At that point the proxy serializes its own Confirm Active and reaches the vulnerable code.

Observed crash output:

[*] launching malicious client (numCellCaches=0) Aborted (core dumped) freerdp-proxy proxy.ini [*] proxy exit status: 134 [malclient] connecting to 127.0.0.1:39100 with BitmapCacheV2NumCells=0 [backend] peer accepted REPRO_RESULT: reproduced

The fatal assertion places the crash in the proxy's client thread during freerdp_connect:

[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((cellInfo)) [libfreerdp/core/capabilities.c:rdp_write_bitmap_cache_cell_info:2019]

The run matrix documented in the advisory:

Declared cell countTest conditionReported result
5Persistence disabledSurvives as the baseline.
2Persistence disabledSurvives, but propagates count 2 upstream to the backend.
0Published crash reproductionDeterministic SIGABRT, exit status 134 (NULL dereference).
1, 2, or 3Persistence enabledAborts at the index 4 read.
4Persistence enabledSurvives depending on adjacent heap contents.

With counts of 1, 2, or 3 and persistent keys enabled, the proxy failed on an out-of-bounds read of index 4:

[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((settings->BitmapCacheV2CellInfo[4].numEntries <= 65535)) [libfreerdp/core/activation.c:rdp_send_client_persistent_key_list_pdu:374]

The advisory notes a limitation: the test libraries had no AddressSanitizer runtime, so the primary heap out-of-bounds write and read sink was not instrumented. The captured crashes are the deterministic NULL dereference (count 0) and the activation.c:374 out-of-bounds read assertion (counts 1 to 3 with persistent keys). Together they establish an array shorter than the five elements the writers index. A connection that survives the test is not evidence that the shortened allocation is safe.

Patch information

The fix shipped in FreeRDP 3.32.2, released 12 October 2026. The maintainer advisory GHSA-xc85-4mqc-xgcx lists versions through 3.32.1 as affected and 3.32.2 as patched, the 3.32.2 ChangeLog includes this GHSA in its CVE list, and NVD links the same advisory to CVE-2026-121952. The advisory does not name a fix commit. The commits below are part of the 3.32.2 tag history, were committed together on 2 October 2026, and change the exact functions the advisory identifies as unsafe. The mapping from advisory to commits is our own reading of the history.

The patch does not tighten the parser as its primary defense. Instead, it makes every consumer stop trusting the array length.

1. The writer copies into a fixed-size stack array (commit 87beb0a9)

The commit message states it plainly: "Do not assume all cellinfo entries are in use. Use a fixed size stack copy to send." The writer now reads BitmapCacheV2NumCells, copies only that many entries into a zero-initialized local array of five, and serializes the local array:

+ const UINT32 BitmapCacheV2NumCells = + freerdp_settings_get_uint32(settings, FreeRDP_BitmapCacheV2NumCells); + BITMAP_CACHE_V2_CELL_INFO CellInfo[5] = WINPR_C_ARRAY_INIT; ... - settings->BitmapCacheV2CellInfo[4].persistent = 1; + for (UINT32 x = 0; x < BitmapCacheV2NumCells; x++) + { + BITMAP_CACHE_V2_CELL_INFO* cur = &settings->BitmapCacheV2CellInfo[x]; + if (persist) + cur->persistent = 1; + CellInfo[x] = *cur; + } ... + for (UINT32 x = 0; x < ARRAYSIZE(CellInfo); x++) + { + if (!rdp_write_bitmap_cache_cell_info(s, &CellInfo[x])) + return FALSE; + }

This closes all three outcomes described in the advisory. The four-byte heap write is gone because the persistent = 1 store now happens only inside a loop bounded by the real element count. The heap disclosure to the backend is gone because slots beyond the declared count are zero bytes from the local array, not leftover heap memory. The NULL dereference at count 0 is gone because the copy loop runs zero times and never touches the NULL pointer. The wire format still always carries five TS_BITMAPCACHE_CELL_CACHE_INFO fields, with unused slots zeroed.

The same commit makes rdp_write_bitmap_cache_cell_info return BOOL and call Stream_EnsureRemainingCapacity(s, 4) before every write. In freerdp_settings_new, the old hand-rolled calloc(6, ...) default is replaced with freerdp_settings_set_pointer_len(..., CellInfo, ARRAYSIZE(CellInfo)), so allocation size and stored length always match. It also adds a NULL check in bitmap_cache_resize in libfreerdp/cache/bitmap.c:

freerdp_settings_get_pointer_array(settings, FreeRDP_BitmapCacheV2CellInfo, i); + if (!info) + return FALSE;

2. The persistent key list path refuses inconsistent settings (commit 198d6854)

This function needs all five caches, so the fix is a hard check rather than a bounded loop:

+ const UINT32 cellInfoCount = + freerdp_settings_get_uint32(settings, FreeRDP_BitmapCacheV2NumCells); + if (cellInfoCount != 5) + { + WLog_ERR(TAG, "BitmapCacheV2NumCells %" PRIu32 ", but must be 5. Aborting.", cellInfoCount); + free(keyList); + return FALSE; + }

If a client advertised fewer than five caches, the proxy now aborts this PDU cleanly instead of reading BitmapCacheV2CellInfo[4] from a shorter allocation.

The same day, rdp_read_bitmap_cache_cell_info gained a table of per-cell limits from [MS-RDPBCGR] 2.2.7.1.4.2 ({ 600, 600, 65536, 4096, 2048 }) and a limit parameter. As committed, an oversized numEntries only produces a WLOG_ERROR message and the function still returns TRUE, so this change adds diagnostics rather than a hard rejection. Commits 1 and 2 are what close the out-of-bounds write.

The advisory suggested two options: reject any count other than five in the parser, or always keep the array at five entries. The maintainers instead made each consumer either respect the stored length (the capability writer) or require exactly five (the persistent key list). That holds regardless of how the shortened length enters the settings, including the proxy's path through freerdp_settings_int_buffer_copy into the upstream client context. Proxy operators relying on vendor backports should check for commits 87beb0a9 and 198d6854, which remove the fixed-index accesses.

Detection methods

There is no ready-made Snort, Suricata, Sigma, YARA, or Nuclei signature for CVE-2026-121952, and no campaign IoCs (IPs, domains, or hashes) have been published. The issue is not listed as known to be exploited. The FreeRDP advisory does, however, support three layers of detection: finding exposed assets, inspecting protocol traffic, and alerting on crash logs.

1. Know where to look: only the proxy is exposed

The vulnerable path runs only in FreeRDP's proxy component (freerdp-proxy, built with -DWITH_PROXY=ON). A client sends a Confirm Active PDU whose BITMAP_CACHE_V2 capability set declares numCellCaches below 5. The proxy accepts it, because the parser only rejects values above 5, then reallocates BitmapCacheV2CellInfo to the smaller size and copies that setting into the upstream client context. When it writes its own Confirm Active to the backend, it accesses fixed indices 0 through 4. Start by listing every host running freerdp-proxy older than 3.32.2. Ordinary FreeRDP clients and servers are not the target of this CVE.

2. Network and protocol signal: Confirm Active with numCellCaches from 0 to 4

The trigger is a single field. In a client-to-proxy Confirm Active, the Bitmap Cache Rev2 capability set carries a numCellCaches value from 0 to 4 instead of the 5 the proxy's writers expect. You can observe this in two places:

  • On the front end (client to proxy): a decrypting sensor or an instrumented proxy can parse the capability set and flag numCellCaches < 5. These sessions are usually TLS-protected, and NLA may also apply, so a passive IDS without decryption will not see the field.
  • On the backend (proxy to backend): often easier. The advisory shows that the smaller length is passed upstream. With numCellCaches=2 and no persistent keys, the proxy did not crash, but the backend received numCellCaches=2 from the proxy. If your backend RDP servers can log negotiated client capabilities, a Confirm Active from the proxy declaring fewer than 5 cell caches is a strong sign that a front-end client sent the malformed value, and adjacent proxy heap data may also have been serialized into that PDU.

None of the sources state whether legitimate RDP clients ever send fewer than 5 cell caches. Baseline what your normal clients advertise before alerting on this as a high-confidence rule.

3. Host and log signal: WinPR assertion failures and SIGABRT

The most practical detection is crash and log monitoring on the proxy host. In the advisory's reproduction, numCellCaches=0 caused a deterministic abort with this log entry:

[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((cellInfo)) [libfreerdp/core/capabilities.c:rdp_write_bitmap_cache_cell_info:2019]

The backtrace that followed contained a dli_sname=freerdp_connect frame, showing the crash happened in the proxy's client thread while writing its Confirm Active. With numCellCaches set to 1, 2, or 3 and persistent bitmap caching enabled by the client, the proxy instead failed on an out-of-bounds read of index 4:

[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((settings->BitmapCacheV2CellInfo[4].numEntries <= 65535)) [libfreerdp/core/activation.c:rdp_send_client_persistent_key_list_pdu:374]

You can turn this into alerts:

  • Log rules: watch for com.freerdp.winpr.assert FATAL entries that mention rdp_write_bitmap_cache_cell_info or rdp_send_client_persistent_key_list_pdu, or that reference BitmapCacheV2CellInfo.
  • Process and service monitoring: the proxy aborted with SIGABRT, wrote a core dump, and exited with status 134. Alert when freerdp-proxy restarts unexpectedly, when systemd reports abnormal exits, or when a core dump appears, then match the crash time to the preceding client connection.
  • Correlating with the backend: at the moment of the crash, the backend logged the peer as accepted and then a connection reset. A backend session that resets right after the proxy connects, together with a proxy abort, matches this bug.

4. A crash-only view will miss some attacks

The advisory's results show why crash detection alone is insufficient. With numCellCaches=4 and persistent keys enabled, the proxy survived because nearby heap contents happened to pass the assertion. With numCellCaches=2 and no persistent keys, the proxy kept running while sending the smaller array upstream. The four-byte out-of-bounds heap write itself produces no log line, and the researcher's build had no sanitizer, so the write was never captured. A proxy that never crashes may still have had heap memory corrupted or leaked, which is why the protocol-level check is worth adding alongside crash alerting.

5. Validating your detections

To test these alerts before deploying them, build the proxy with AddressSanitizer in a lab. That makes the silent out-of-bounds write visible, which the researcher's non-instrumented run could not do. Then confirm that your log, process, and backend-capability alerts all fire for the values the advisory tested: 0, values from 1 to 4 with persistent keys enabled, and 2 without.

Affected systems and versions

  • Affected: FreeRDP versions up to and including 3.32.1 (the advisory gives the vulnerable range as <= 3.32.1, and VulnCheck independently identifies versions before 3.32.2 as affected).
  • Patched: FreeRDP 3.32.2, released 12 October 2026.
  • Scope: only the FreeRDP proxy component (freerdp-proxy, built with -DWITH_PROXY=ON) exercises the vulnerable path. Ordinary FreeRDP clients and servers are not the target of this CVE.
  • Distribution packaging: Amazon Linux 2023 ships the fix as freerdp 3.32.2-1.amzn2023 for aarch64 and x86_64 via advisory ALAS2023-2026-3342 (release version 2023.12.20261029). The cited sources do not establish whether other distributions ship equivalent fixes.
  • Branch support: the project security policy supports the 3.x stable branch and the development branch, while 2.x and older branches are marked unsupported.

Vendor security history

FreeRDP has a documented history of both serious memory safety defects and coordinated fixes. Check Point's earlier study examined FreeRDP 2.0.0-rc3, reporting six vulnerabilities overall, including five with major security impact, and demonstrated exploitation of CVE-2018-8786 involving a malicious RDP server and memory corruption in the client. That historical code execution result runs in the opposite direction (a malicious server attacking a client) and is separate from the evidence available for CVE-2026-121952, where a malicious client attacks the proxy. The same disclosure history records that FreeRDP began patch work on the day it received the report, that researchers verified the fixes, and that patches were incorporated into 2.0.0-rc4. GitHub Security Lab's later FreeRDP research reported 12 bugs, including memory safety failures, found using AFL or AFL++, AddressSanitizer, and UndefinedBehaviorSanitizer, and credited the maintainer's cooperation. This record supports a pattern of coordinated remediation, but it does not imply that later protocol handling paths are free of similar memory safety defects.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization