{"schema":"threatline.record/2","record":{"id":"GHSA-xq87-9rrm-6wqw","aliases":["GHSA-xq87-9rrm-6wqw"],"summary":"FreeRDP rdpecam client accepts an unadvertised media type and allocates from server dimensions","details":"## Executive Summary\r\n\r\nMallory controls an RDP server to which a FreeRDP client connects with camera redirection enabled. The client advertises only H.264 320x240 at 30 frames per second, but the server can send a `StartStreamsRequest` for H.264 4096x4096 at 30 frames per second. In the assessed rdpecam client, that request is accepted, a 67,108,864-byte pending-sample buffer is allocated, and the local camera HAL is called with the server-selected dimensions. This is a server-to-client resource-exhaustion and camera-reconfiguration primitive; it is not an authentication bypass, a session theft issue, or demonstrated memory corruption.\r\n\r\nThe required configuration is a build with the rdpecam client enabled, a local camera, and camera redirection enabled. The exact source reviewed is FreeRDP revision `fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3`, described by the repository as `3.31.1-284-gfe8e4a68c`. The behavior is also present in the inspected `3.30.0`, `3.31.0`, and `3.31.1` release snapshots. `3.30.0` is the earliest release snapshot inspected, not a proven first affected release, and no fixed release was established from the available history.\r\n\r\nI reviewed the exact source revision, the inspected release snapshots, the source harness, and its preserved runtime output. The runtime proof uses the real rdpecam client path for channel dispatch, wire parsing, encoder initialization, and allocation, but stubs the DVC transport and camera HAL; it therefore demonstrates acceptance and allocation in production code, not a full hardware-backed end-to-end camera failure.\r\n\r\n## Background\r\n\r\nThe rdpecam client first answers the server's media-type-list request. In the supplied runtime, the only advertised descriptor is H.264 320x240 at 30/1 with a 1/1 pixel aspect ratio. The server then chooses a media type in `StartStreamsRequest`. The security-relevant expectation is that this choice is constrained to a descriptor the client advertised and accepted for the selected stream.\r\n\r\nThis path is conditional rather than a default property of every FreeRDP build. The client must be built with `CHANNEL_RDPECAM_CLIENT` enabled, the user must have a local camera, and camera redirection must be enabled for the RDP connection. The attacker is the remote RDP server, typically reached when a user connects to a malicious or compromised server. No separate victim interaction is required after the connection is established.\r\n\r\n## Vulnerability Details\r\n\r\nIn `channels/rdpecam/client/camera_device_main.c`, `ecam_dev_read_media_type` validates that the format and enum-like fields are valid and that the rate and pixel-aspect denominators are nonzero, but it does not compare the descriptor with the list previously advertised for the stream:\r\n\r\n```c\r\nstatic BOOL ecam_dev_read_media_type(wStream* s, CAM_MEDIA_TYPE_DESCRIPTION* mediaType)\r\n{\r\n\tconst uint8_t format = Stream_Get_UINT8(s);\r\n\tif (!rdpecam_valid_CamMediaFormat(format))\r\n\t\treturn FALSE;\r\n\r\n\tmediaType->Format = WINPR_ASSERTING_INT_CAST(CAM_MEDIA_FORMAT, format);\r\n\tmediaType->Width = Stream_Get_UINT32(s);\r\n\tmediaType->Height = Stream_Get_UINT32(s);\r\n\tmediaType->FrameRateNumerator = Stream_Get_UINT32(s);\r\n\tmediaType->FrameRateDenominator = Stream_Get_UINT32(s);\r\n\tmediaType->PixelAspectRatioNumerator = Stream_Get_UINT32(s);\r\n\tmediaType->PixelAspectRatioDenominator = Stream_Get_UINT32(s);\r\n\r\n\tif (mediaType->FrameRateNumerator == 0)\r\n\t\treturn FALSE;\r\n\tif (mediaType->FrameRateDenominator == 0)\r\n\t\treturn FALSE;\r\n\tif (mediaType->PixelAspectRatioNumerator == 0)\r\n\t\treturn FALSE;\r\n\tif (mediaType->PixelAspectRatioDenominator == 0)\r\n\t\treturn FALSE;\r\n```\r\n\r\nThose checks are in the assessed revision at lines 89-112 of `channels/rdpecam/client/camera_device_main.c`. The request handler then treats the parsed, peer-controlled descriptor as the stream configuration. It stores it, initializes the encoder context, allocates a pending sample using the requested width and height, and passes the resulting descriptor to `StartStream`:\r\n\r\n```c\r\n\tstream->currMediaType = mediaType;\r\n\r\n\tif (!ecam_encoder_context_init(stream))\r\n\t\treturn ERROR_INVALID_DATA;\r\n\r\n\tmediaType.Format = streamInputFormat(stream);\r\n\tstream->pendingSample = Stream_New(nullptr, 4ull * mediaType.Width * mediaType.Height);\r\n\tif (!stream->pendingSample)\r\n\t\treturn ERROR_INVALID_DATA;\r\n\r\n\tstream->captureRunning = TRUE;\r\n\tconst CAM_ERROR_CODE error = dev->ihal->StartStream(\r\n\t\tdev->ihal, dev, streamIndex, &mediaType, ecam_dev_sample_captured_callback);\r\n```\r\n\r\nThis is `ecam_dev_process_start_streams_request` at lines 393-439. The dispatch in the same file sends `CAM_MSG_ID_StartStreamsRequest` directly to this handler at lines 843-845. The list-response path records the selected format-conversion pair and sends the descriptors at lines 684-699, but no later comparison of a received descriptor with the sent descriptor list occurs. Thus a validly encoded descriptor can be outside the client's advertised dimensions and still reach encoder setup, allocation, and camera start.\r\n\r\nThe supplied runtime exercised this causal sequence with the real production channel dispatcher and wire parser: the client emitted a 320x240 descriptor, received a 4096x4096 request, returned success, initialized the production encoder context, allocated the production pending sample, and invoked the HAL callback. The DVC transport and camera HAL were stubs used to isolate the client; the production channel dispatch, NDR parsing, encoder context, and allocation executed. The separate MJPEG decode-corruption hypothesis was not demonstrated and is outside this report.\r\n\r\n## Exploitability Analysis\r\n\r\nThe narrow demonstrated primitive is remote control of one camera stream's accepted geometry and the resulting client allocation. In the preserved run, the server-selected 4096x4096 geometry produced exactly `4 * 4096 * 4096 = 67,108,864` bytes. A malicious server can repeat the request across connections or use larger protocol-representable values, subject to allocation failure and the client's normal cleanup behavior. The source handles a failed `Stream_New` by stopping the stream and returning an error, so this evidence does not establish a memory-safety write or a reliable process crash.\r\n\r\nThe run also showed that the HAL callback receives the unadvertised dimensions. Because that HAL was deliberately a stub, the evidence does not prove what a particular V4L2 or Android camera driver will do with those dimensions. Source review does establish that the production call is made; hardware-specific camera churn and a full denial of service remain conditional.\r\n\r\nThe meaningful positive control is the client's own media-list response: it contained only 320x240@30, while the subsequent 4096x4096 request returned success. A negative control using an exact advertised `StartStreamsRequest`, a repeated-request stress test, and a real camera backend were not performed in this artifact set. Those controls would measure deployment-specific availability effects, not whether the missing media-type comparison exists.\r\n\r\n## Proof of Concept\r\n\r\nThe source-only PoC is [`poc/rdpecam_unadvertised_media_type.c`](poc/rdpecam_unadvertised_media_type.c). It creates a real `CameraDevice`, accepts a channel through the production callback, sends a media-type-list request, and then sends an unadvertised 4096x4096 start request. It is intended to be compiled in a FreeRDP build with the rdpecam client sources and production libraries available; the harness source alone is not a standalone build recipe.\r\n\r\nThe preserved output, with local build paths removed, is:\r\n\r\n```text\r\n[client->server] MediaTypeListResponse raw (28 bytes): 01 0c 01 40 01 00 00 f0 00 00 00 1e 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01\r\n[HAL] StartStream called with server-chosen media type: format=1 4096x4096 fps=30/1 PAR=1/1\r\n[client] StartStreamsRequest(advertised=320x240, requested=4096x4096) -> rc=0\r\n[client] real pendingSample allocation: capacity=67108864 bytes (4*W*H=67108864)\r\n```\r\n\r\nThese lines show the advertised/requested mismatch, the successful handler return, the production allocation size, and the peer dimensions reaching the HAL boundary. The DVC transport and camera HAL are stubs; production channel dispatch, NDR parsing, encoder context initialization, and allocation execute. The harness frees the device after the check, but a malicious server can request a large allocation repeatedly. Do not run it against a production camera client without a disposable process and resource limits.\r\n\r\n## Remediation\r\n\r\nBefore calling `ecam_encoder_context_init`, allocating `pendingSample`, or invoking `StartStream`, match every field of the received media descriptor against the descriptor list advertised and accepted for the same stream: format, width, height, frame-rate numerator and denominator, pixel aspect ratio, and relevant flags. Return an invalid-message or unsupported-media error for a mismatch. Keep the accepted list in stream state rather than retaining only a conversion pair or a single current descriptor.\r\n\r\nThe implementation should also perform checked multiplication and an explicit maximum allocation bound before evaluating `4 * width * height`. These are defense-in-depth measures; they do not replace the required list-membership check. Regression tests should cover an exact advertised descriptor, an unadvertised size with an otherwise valid format, a mismatched frame rate or aspect ratio, zero and malformed fields, and a large request that must be rejected before encoder setup, allocation, and HAL invocation.\r\n\r\nNo fix was present in the assessed revision or in the inspected 3.30.0, 3.31.0, and 3.31.1 release snapshots. A fixed release and backport are therefore not claimed here.\r\n\r\n## Summary\r\n\r\nWith the rdpecam client enabled, a local camera present, and camera redirection enabled, a malicious RDP server can send a valid but unadvertised `StartStreamsRequest`. FreeRDP accepts the peer's width and height, allocates `4 * W * H`, and passes those values to the camera HAL. The runtime evidence demonstrated a 4096x4096 request and a 67,108,864-byte production buffer while using stub DVC transport and camera hardware; it did not demonstrate memory corruption or a hardware-independent guaranteed crash. The behavior was verified in release snapshots 3.30.0, 3.31.0, and 3.31.1 and in revision `3.31.1-284-gfe8e4a68c`; no fixed release was established.","categories":["Unclassified"],"product":"freerdp","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-xq87-9rrm-6wqw","source_modified_at":"2026-10-01T11:21:02Z","fetched_at":"2026-10-23T19:00:29.328130Z","package":{"ecosystem":"c","name":"freerdp"},"range":">= 3.30.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-xq87-9rrm-6wqw","source_modified_at":"2026-10-01T11:21:02Z","fetched_at":"2026-10-23T19:00:29.328130Z","package":{"ecosystem":"c","name":"freerdp"},"version":"3.32.0"}],"cvss":5.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-01T11:21:02Z","modified_at":"2026-10-01T11:21:02Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-xq87-9rrm-6wqw","fetched_at":"2026-10-23T19:00:29.328130Z","source_modified_at":"2026-10-01T11:21: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-10-01T11:21:02Z","content_hash":"86b9f6e77fc63aeadd74ea9dd823b08a7708ccd80742754d118ccf9aa6daf31f"}}
