All vulnerabilities
GHSA-xq87-9rrm-6wqw

FreeRDP rdpecam client accepts an unadvertised media type and allocates from server dimensions

freerdp
CVSS
5.3Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.32.0Fixed versionGitHub
Published1 Oct 2026Updated 1 Oct 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Executive Summary

Mallory 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.

The 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.

I 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.

Background

The 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.

This 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.

Vulnerability Details

In 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:

static BOOL ecam_dev_read_media_type(wStream* s, CAM_MEDIA_TYPE_DESCRIPTION* mediaType)
{
	const uint8_t format = Stream_Get_UINT8(s);
	if (!rdpecam_valid_CamMediaFormat(format))
		return FALSE;

	mediaType->Format = WINPR_ASSERTING_INT_CAST(CAM_MEDIA_FORMAT, format);
	mediaType->Width = Stream_Get_UINT32(s);
	mediaType->Height = Stream_Get_UINT32(s);
	mediaType->FrameRateNumerator = Stream_Get_UINT32(s);
	mediaType->FrameRateDenominator = Stream_Get_UINT32(s);
	mediaType->PixelAspectRatioNumerator = Stream_Get_UINT32(s);
	mediaType->PixelAspectRatioDenominator = Stream_Get_UINT32(s);

	if (mediaType->FrameRateNumerator == 0)
		return FALSE;
	if (mediaType->FrameRateDenominator == 0)
		return FALSE;
	if (mediaType->PixelAspectRatioNumerator == 0)
		return FALSE;
	if (mediaType->PixelAspectRatioDenominator == 0)
		return FALSE;

Those 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:

	stream->currMediaType = mediaType;

	if (!ecam_encoder_context_init(stream))
		return ERROR_INVALID_DATA;

	mediaType.Format = streamInputFormat(stream);
	stream->pendingSample = Stream_New(nullptr, 4ull * mediaType.Width * mediaType.Height);
	if (!stream->pendingSample)
		return ERROR_INVALID_DATA;

	stream->captureRunning = TRUE;
	const CAM_ERROR_CODE error = dev->ihal->StartStream(
		dev->ihal, dev, streamIndex, &mediaType, ecam_dev_sample_captured_callback);

This 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.

The 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.

Exploitability Analysis

The 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.

The 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.

The 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.

Proof of Concept

The 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.

The preserved output, with local build paths removed, is:

[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
[HAL] StartStream called with server-chosen media type: format=1 4096x4096 fps=30/1 PAR=1/1
[client] StartStreamsRequest(advertised=320x240, requested=4096x4096) -> rc=0
[client] real pendingSample allocation: capacity=67108864 bytes (4*W*H=67108864)

These 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.

Remediation

Before 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.

The 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.

No 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.

Summary

With 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.

Text from GitHub Advisory Database

Affected versions 1

  • freerdp, c>= 3.30.0, <= 3.31.1GitHub