FreeRDP client accepts a negotiated security protocol outside its enabled set (PROTOCOL_RDSTLS, PROTOCOL_HYBRID_EX)
What fixes it
- 3.32.0FreeRDP, cGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics.
CWE-757 (Selection of Less-Secure Algorithm During Negotiation), contributing CWE-522 (Insufficiently Protected Credentials) and CWE-1390 (Weak Authentication).
Relationship to GHSA-x7v6-xfx3-52j6 (distinct; not fixed by it)
This is not the "unintended RDSTLS" issue fixed in 3.31.0. GHSA-x7v6-xfx3-52j6 is a server-side bug in rdp_server_accept_nego(): on a negotiation failure the server stores PROTOCOL_FAILED_NEGO (0x80000000) | HYBRID_REQUIRED_BY_SERVER (0x5) in SelectedProtocol, and the value 0x80000005 happens to contain the PROTOCOL_RDSTLS bit (0x4), so a failure was dispatched as an RDSTLS selection. Its fix adds a terminal-failure guard (if (SelectedProtocol & PROTOCOL_FAILED_NEGO) return FALSE) — it does not add any check that a negotiated protocol is in the client's enabled set.
This finding is on the client side and is a different root cause: nego_process_negotiation_response() accepts whatever selectedProtocol the peer puts in a well-formed RDP_NEG_RSP (here PROTOCOL_RDSTLS 0x4 or PROTOCOL_HYBRID_EX 0x8) without ever comparing it against nego->EnabledProtocols[]. Because the 3.31.0 fix never touched this path, the downgrade still reproduces at current master (see Evidence): a client started with /sec:nla (only PROTOCOL_HYBRID enabled) proceeds into the RDSTLS/HYBRID_EX path a hostile peer selects.
Summary
When the client parses the server's X.224 negotiation response, the selected protocol is checked against nego->EnabledProtocols[] only for PROTOCOL_RDSAAD, PROTOCOL_HYBRID, PROTOCOL_SSL and (in the else branch) PROTOCOL_RDP. PROTOCOL_RDSTLS (0x04) and PROTOCOL_HYBRID_EX (0x08) are not compared against the enabled set, and nego_set_selected_protocol() does not validate either, so a peer can select a protocol the client neither requested nor enabled.
A client started with /sec:nla has EnabledProtocols[] == { PROTOCOL_HYBRID } while NegotiateSecurityLayer remains TRUE, so the response is still parsed and honoured. A peer that answers selectedProtocol = PROTOCOL_RDSTLS:
- passes the ungated branch and stays in
NEGO_STATE_FINAL; - has
nego_update_settings_from_state()(nego.c:1701-1717) rewrite the client's policy to
TlsSecurity=TRUE, NlaSecurity=FALSE, RdstlsSecurity=TRUE, EncryptionLevel=NONE — the user's NLA pinning is discarded;
- is reached via
nego_try_connect()(nego.c:342) →transport_connect_rdstls(); - receives an RDSTLS authentication request that carries the pre-allocated all-zero
ServerAutoReconnectCookie (rdstls_write_authentication_request_with_cookie, rdstls.c:449-453, sessionId=0, 28 zero bytes), so nothing has to be guessed;
- replies
RDSTLS_RESULT_SUCCESS, which the client accepts (rdstls.c:1009); - then receives the Client Info PDU, which for a TLS-protected non-CredSSP session carries
settings->Password in cleartext (rdp_write_info_packet, libfreerdp/core/info.c).
The same downgrade attempted with PROTOCOL_SSL is refused by the client, because that value is one of the four the block does gate. PROTOCOL_HYBRID_EX is the second ungated value; it routes to transport_connect_nla(transport, TRUE) and so remains CredSSP — policy-relevant rather than credential-disclosing, but the same missing check.
Primitive
Attacker-controlled input: the 4-byte selectedProtocol field of the X.224 Connection Confirm, i.e. the first server-to-client packet. It influences a branch (nego->SelectedProtocol → security-layer dispatch) and the client's whole security policy. Result: a client-configured security requirement is overridden by the peer, and the user's reusable password is disclosed in cleartext to that peer.
Preconditions and scope caveats
- The attacker must be able to answer the client's negotiation: an on-path position, a hostile
RD Gateway/proxy, or a server the user was induced to connect to.
- The attacker's TLS certificate must be accepted by the client —
/cert:ignore, a chain the client
trusts, or a user who clicks through the TOFU / changed-certificate prompt. Certificate validation runs before the RDSTLS handshake, so this is the effective barrier.
- Affects clients that pin
/sec:nla(or otherwise leave a single protocol enabled) without also
passing -nego.
- The RDSTLS authentication request itself only discloses an all-zero cookie; the password is
disclosed later, in the Client Info PDU.
We rate this Medium; the final rating is yours.
Evidence (validated run at 7e0a52a (current master, 2026-09-14))
victim command line : sfreerdp /v:127.0.0.1:3389 /cert:ignore /sec:nla /u:admin /p:'SuperSecret123!'
victim policy : /sec:nla => EnabledProtocols[] = { PROTOCOL_HYBRID } only
[attacker] victim X.224 Connection Request: requestedProtocols=0x00000003
[attacker] answering with selectedProtocol=0x00000004 (RDSTLS) <-- NOT in the client's enabled set
[attacker] TLS handshake COMPLETED with the victim (client proceeded into the RDSTLS path)
[attacker] victim sent an RDSTLS authentication request (40 bytes):
version=0x0001 type=0x0002 dataType=0x0002
RDSTLS_DATA_AUTORECONNECT_COOKIE sessionId=0 cookieLen=28
cookie=00000000000000000000000000000000000000000000000000000000
[attacker] answered RDSTLS_RESULT_SUCCESS -- the victim now believes it is authenticated
=========== server-side credential snapshot #2 (server settings after Client Info PDU decode) ===========
domainName [0] : "(null)"
userName [1] : "admin"
password [2] : "SuperSecret123!"
RESULT: PLAINTEXT PASSWORD RECEIVED BY THE SERVER
==============================================================Negative control, same peer answering selectedProtocol = PROTOCOL_SSL (0x01):
[attacker] no TLS handshake from the victim ... client refused the offered protocol before starting TLS [ERROR][com.freerdp.core.nego] - [nego_connect]: Protocol Security Negotiation Failure [ERROR][com.freerdp.core] - [rdp_client_connect]: ERRCONNECT_SECURITY_NEGO_CONNECT_FAILED [0x0002000C]
Third case, selectedProtocol = PROTOCOL_HYBRID_EX (0x08):
[attacker] TLS handshake COMPLETED with the victim (client proceeded into the HYBRID_EX path) [attacker] RESULT: client accepted selectedProtocol=0x08 although it is not in EnabledProtocols[]
Reproducer archive (attached)
Attached as ANT-2026-D7RY7FA7-e2e.zip. It contains a self-contained docker-e2e/ build context (Ubuntu 24.04, clang-20, FreeRDP built from source at ARG TARGET_COMMIT), an external hostile peer written in plain Python (no FreeRDP code), and a credential-printing server that is a consumer of FreeRDP's public server API (listener.h/peer.h) configured with FreeRDP_TlsSecurity=TRUE. No FreeRDP source is patched, stubbed or reimplemented; the victim is FreeRDP's own sfreerdp.
unzip ANT-2026-D7RY7FA7-e2e.zip && cd ANT-2026-D7RY7FA7 docker build -t ant-d7ry7fa7 docker-e2e/ docker run --rm ant-d7ry7fa7
Build it at another commit with --build-arg TARGET_COMMIT=<sha>. The run takes a few minutes and prints the three cases above in order. One note on the harness: after the RDSTLS handshake the attacker relays the victim's stream to the credential-printing server and rewrites clientCoreData::serverSelectedProtocol from 0x04 to 0x01 on the wire, because gcc.c:1335 makes that backend reject a stream whose echoed protocol differs from the one it itself negotiated. That rewrite is attacker-side bookkeeping in the relay, not a change to FreeRDP.
Credit
Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics. Please credit Claude and Ada Logics in any advisory or release note.
Handled under Anthropic's coordinated vulnerability disclosure policy (https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Questions to security-cvd@anthropic.com.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c<= 3.31.0GitHub