Multiple vulnerabilities in the Xpra client
09/07/2026 - Téléchargement
Product
Xpra client
Severity
Critical
Fixed Version(s)
5.1.6
6.5.1
Affected Version(s)
5.* <= 5.1.5
6.* <= 6.5.0
CVE Number
N/A
Authors
Description
Presentation
Xpra ("X Persistent Remote Applications") is an open-source multi-platform tool that runs graphical applications remotely and forwards their display to a local client over a network transport (TCP, SSH, WebSocket, …). It is frequently described as "screen for X11": the server keeps the applications running while the user attaches and detaches a client with xpra attach <address>. The client is available on Linux, Windows and macOS.
Beyond the display itself, Xpra exposes a rich set of integration features between the server and the client: clipboard synchronisation, file transfer, remote printing, opening of files and URLs, webcam and audio forwarding, a server-driven system-tray menu, remote logging, and a control management channel.
Issue(s)
During an internal security assessment, Synacktiv reviewed Xpra under the threat model of a legitimate client connecting to a malicious (rogue) server, i.e. the user runs xpra attach <address> against an attacker controlled endpoint. Both the client and the server ran Xpra 5.1.2 on Linux; all findings were re-confirmed against the source of 5.1.5. Version 5.1 is the LTS at the time of writing.
The review identified that, in its default configuration, the Xpra client trusts the server far more than this threat model allows. Several server to client features that are security-sensitive are reachable by default, and the authorization logic that is supposed to gate them can be bypassed. Combined, these issues let a rogue server, or a single malicious link, gain remote code execution on the client, generally with no user interaction.
The following issues were identified:
- Insecure default configuration: security-sensitive features (clipboard with bidirectional synchronization, file transfer, open-files, open-url, remote logging, server-driven
controlchannel) are enabled or set toautoby default, widening the attacker-reachable surface. - URL-scheme handler option injection (
xpra://,xpra+ssh://, …): a single link click runs attacker-controlled commands on the victim, or silently connects the client to a rogue server. - File-transfer user consent bypass using
controlmessages: the ungatedcontrol show_bug_reportserver message and therequest-fileauto-accept logic let the server have a pushed file accepted (and opened) without any user consent prompt. - File-transfer user consent bypass through the
printitattribute, allowing a server initiated file write without user consent prompt. - Dangerous downloads directory fallback: when
~/Downloadsdoes not exist, downloads land directly in the user's$HOME. open-urlconfiguration bypass through object kind confusion, allowing the server to open an arbitrary, server-controlled URL regardless of theopen-urlpolicy.- Image and video encoding allow-list bypass: the server chooses the decoder and supplies the entire bitstream through the mandatory
drawpacket, reaching memory-unsafe C codecs even when they are not in the client's configured encoding set.
Affected versions
Versions 5.1.2 to 5.1.5 are affected, and anterior versions are likely to be vulnerable as well. Some findings also applies to version 6.* before version 6.5.1.
Timeline
| Date | Description |
|---|---|
| 2026-06-25 | First contact with Xpra developers. |
| 2026-06-26 | Xpra developers acknowledges the report and push fixes to both supported versions of Xpra. |
| 2026-07-02 | Synacktiv confirms most of the findings will be fixed in the next release. |
| 2026-07-03 | Xpra version 5.1.6 and 6.5.1 are released. |
Technical details
Finding 1: Insecure default configuration
Description
By default, the Xpra client enables a number of server→client features that significantly broaden the attack surface reachable by a malicious server. The shipped configuration (/etc/xpra/conf.d/) sets, among others:
# /etc/xpra/conf.d/05_features.conf
clipboard = yes
clipboard-direction = both
remote-logging = both
# /etc/xpra/conf.d/15_file_transfers.conf
file-transfer = auto
open-url = auto
open-files = auto
The features identified as security-sensitive, because they let the server reach a code-execution or data-exfiltration sink, are:
clipboard # the server can spy on and overwrite the client's clipboard
remote-logging # the server can pull almost all of the client's logs
webcam # local capture device reachable by the server
file-transfer # the server can push files onto the client
open-files # the server can have the client open received files
open-url # the server can have the client open URLs with the default handler
encodings # the server can reach the installed decoders (see finding 7)
Impact
The default configuration trusts the server far beyond what the "client attaches to an untrusted server" use case warrants. It is the common denominator that makes the remaining findings reachable with little or no user interaction.
Note that the default auto value resolves to "ask", i.e. the feature is enabled but is supposed to be gated behind a user confirmation dialog. As detailed in the following sections, some of those confirmation gates can be bypassed by the server, and the control management channel (which a server can use to drive the client's UI and logging) is registered unconditionally.
Recommendation
Ship a hardened default profile for the client role that disables the sensitive features and minimizes the attacker-reachable parsing surface:
clipboard = no
open-files = no
open-url = no
remote-logging = no
file-transfer = no
encodings = rgb
video-encoders =
video-decoders =
compressors = none,zlib
packet-encoders = rencodeplus
Reducing encodings to rgb and emptying video-encoders/video-decoders removes the native image/video codec attack surface (finding 7); restricting compressors and packet-encoders shrinks the wire-parsing surface the server can reach.
Finding 2: URL-scheme handler injection
Description
Xpra installs a desktop URL-scheme handler (/share/applications/xpra.desktop) that registers itself for many xpra* URL schemes and runs the clicked URL directly through xpra attach:
# /share/applications/xpra.desktop
Exec=xpra attach %u
MimeType=x-scheme-handler/xpra;x-scheme-handler/xpras;x-scheme-handler/xpra+tcp;
x-scheme-handler/xpra+ssh;x-scheme-handler/xpra+ws;x-scheme-handler/xpra+wss; ...
A link such as xpra+ssh://… on any web page, email or chat message therefore becomes, xpra attach <attacker-controlled-URL>.
The URL handler parses the URL query string and applies every valid-typed parameter onto the client options, with no allow-list:
# xpra/scripts/parsing.py:1781
address, params = parse_URL(url) # parse ?k=v
for k, v in validate_config(params).items():
setattr(options, k.replace("-", "_"), v) # apply to opts (no allow-list!)
args[1] = address # query stripped from address
validate_config() only keeps keys that are real Xpra options, but some of those options are command or exec sinks. At least two are both URL-settable and reachable from the attach mode the handler invokes:
ssh, for anxpra+ssh://target, the value ofopts.sshis shell-split and executed locally to establish the connection, so a query parameter can change the ssh binary to any command line such asbash -c 'echo "$(id) at $(date --rfc-3339=seconds)" >> /tmp/xpra_code_exec.log' --.open-command, the program used to open received files/URLs (default/usr/bin/xdg-open) is URL-settable, so a link can set?open-command=<attacker command>.
Impact
A single click on an attacker-controlled link runs arbitrary commands on the victim. Several distinct exploitation paths follow from the option injection above:
-
sshinjection (no server needed). A crafted?ssh=…parameter is executed locally during connection setup, before any server is reached. Thexdg-opencommand below simulates a user opening the malicious link:$ xdg-open 'xprassh://localhost/?ssh=bash%20-c%20\'echo%20%22$(id)%20at%20$(date%20--rfc-3339=seconds)%22%20%3E%3E%20/tmp/xpra_code_exec.log\'%20--' $ cat /tmp/xpra_code_exec.log uid=1000(user) gid=1000(user) groups=1000(user),27(sudo) at 2026-06-25 16:52:40+02:00 -
open-commandinjection. A link sets?open-command=<attacker command>; any subsequent server-triggered open (findings 3 and 6) then runs that command directly, without needing a.desktop/file-association trick.
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least.
Finding 3: File-transfer user consent bypass using control messages
Description
A pushed file is normally gated behind a user-confirmation dialogue. However, when the client has itself previously requested a file, the server may return it and have it accepted without any prompt:
# xpra/net/file_transfer.py:828 function do_process_send_data_request
rf = options.tupleget("request-file")
if rf and len(rf)>=2:
argf, openit = rf[:2]
openit = self.files_requested.pop(bytestostr(argf), None)
if openit is not None:
self.files_accepted[send_id] = openit # auto-accept, no dialog
cb_answer(True)
return
files_requested is meant to be populated only when the client itself calls send_request_file(). The flaw is that this client-side action is server-reachable: the control packet handler is registered unconditionally (xpra/client/gui/ui_client_base.py, add_packet_handlers({... "control": ...})), and its show_bug_report command makes the client request a file:
# xpra/client/gtk3/gtk_client_base.py:598
def download_server_log(self, callback=None):
filename = "${XPRA_SERVER_LOG}"
if callback:
self.file_request_callback[filename] = callback
self.send_request_file(filename, self.open_files) # files_requested["${XPRA_SERVER_LOG}"] = open_files
A rogue server can thus send control show_bug_report to seed files_requested, then satisfy the auto-accept above, completely bypassing the file-transfer consent dialogue that normally protects the client. The accepted file is written to the download directory with a fully server-controlled name and content, and no digest, signature or type check is performed.
Impact
This consent bypass lets the server have an arbitrary file written and, because the openit flag is carried through the auto-accept, handed to the client's open-command (default /usr/bin/xdg-open) through subprocess.Popen:
# xpra/net/file_transfer.py:683
def exec_open_command(self, url:str) -> None:
command = shlex.split(self.open_command)+[url]
...
proc = subprocess.Popen(command, env=self.get_open_env(), shell=WIN32)
Opening a server-controlled file this way does not by itself guarantee code execution: xdg-open dispatches the file to whichever application is associated with its type on the victim's desktop, so the outcome depends on the file handlers installed on the client's host. However, Xpra itself registers a URL and file handler (/share/applications/xpra.desktop, see finding 2) that is exploitable through option injection, and this consent bypass also extends to opening server-controlled URLs (finding 6). The server can therefore steer the open towards Xpra's own xpra+ssh:// handler directly, or by pushing a .xpra file that triggers it turning the consent bypass into reliable remote code execution that does not rely on any third-party application being vulnerable.
The full chain in default configuration is as follows:
- the server sends
control show_bug_report, the client registersfiles_requested["${XPRA_SERVER_LOG}"] = True(default value foropen_files). - the server sends
send-data-requestwithoptions["request-file"]=["${XPRA_SERVER_LOG}", 1], the client matches, setsfiles_accepted[send_id]=Trueand auto-accepts. - the server sends
send-filewith the samesend_id, an attacker filename and content, and norequest-filein its options (so the bug-report callback inprocess_downloaded_filedoes not intercept it).accept_data()popsfiles_accepted[send_id]and returns(False, True), then_open_filecallsexec_open_commandand the open-command runs on the server-controlled file (code execution when steered to thexpra+ssh://handler as described above).
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least.
Finding 4: File-transfer user consent bypass using printit attribute
Description
The file-transfer authorization routine accept_data() places the file-transfer authorization check in an elif branch that is skipped whenever the printit flag is set:
# xpra/net/file_transfer.py:455
def accept_data(self, send_id:str, dtype, basefilename:str, printit:bool, openit:bool) -> Tuple[bool,bool]:
req = self.files_accepted.pop(send_id, None)
if req is not None:
return (False, req)
if printit:
if not self.printing or self.printing_ask:
printit = False # printing off -> silently downgrade to a save
elif not self.file_transfer or self.file_transfer_ask:
return None # <-- SKIPPED whenever printit is True
if openit and (not self.open_files or self.open_files_ask):
openit = False
return (printit, openit)
A malicious server sets printit=True on its send-file. Printing is disabled by default, so printit is downgraded to False, but the return None rejection guarded by file_transfer_ask lives in the skipped elif, so it never runs. accept_data() therefore returns a tuple and the transfer is accepted with no user consent prompt. In other words, declaring a transfer a "print job" skips the file-transfer authorization entirely, and a disabled printer silently turns the print job back into a normal file save.
Impact
In default configuration (file-transfer=auto, open-files=auto) this leads to an arbitrary file write into the download directory, with an attacker-controlled name and content. The write becomes code execution when:
open-files=yes, the survivingopenitflag is honoured, so the written file is opened viaexec_open_command(xdg-open) which leads to command execution using the Xpra URL scheme ; or- combined with the dangerous downloads directory fallback (finding 5), the silent write lands in
$HOMEand can create a login dotfile that is executed on the next login shell, giving zero-click RCE even withopen-filesleft at its default.
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least.
Finding 5: Dangerous downloads directory fallback
Description
The download directory used for received files falls back to the user's $HOME when ~/Downloads does not exist:
# xpra/platform/paths.py:115
def do_get_download_dir() -> str:
d = "~/Downloads"
if not os.path.exists(os.path.expanduser(d)):
return "~" # writes land directly in $HOME
return d
On minimal, freshly-created accounts, or on hosts that are not installed in English, there is frequently no ~/Downloads directory, so any received file lands directly in $HOME. safe_open_download_file() (file_transfer.py:70) uses basename(), which blocks path traversal on Linux, and O_EXCL, which prevents overwriting an existing file. However, neither prevents the creation of a login dotfile that does not yet exist, such as ~/.bash_profile, ~/.profile, ~/.xprofile or ~/.bash_aliases. These files are then evaluated when the user opens a shell.
Impact
On its own, this writes downloaded files into the user's home directory rather than the ~/Downloads dedicated location. Chained with any silent file write, in particular the printit consent bypass (finding 4), it turns a file drop into code execution: the server creates a login dotfile in $HOME which is executed on the user's next login shell, giving remote code execution without requiring open-files=yes.
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least.
Finding 6: open-url configuration bypass through object kind confusion
Description
The opening of a server-supplied URL is supposed to be gated by the open-url / open-url-ask policy. The authorization routine accept_data(), however, decides without ever considering the kind (dtype) of the requested object (file or URL) which produces two object kind confusions.
Confusion 1: a URL open is gated by the file-transfer flags. More specifically, _process_open_url calls accept_data() with dtype="url":
# xpra/net/file_transfer.py:729
def _process_open_url(self, packet):
send_id = net_utf8(packet[2]); url = net_utf8(packet[1])
if not self.open_url:
return
if not self.open_url_ask or self.accept_data(send_id, "url", url, False, True):
self._open_url(url)
But accept_data() never looks at dtype; for printit=False it decides purely on the file-transfer flags:
# xpra/net/file_transfer.py:455
req = self.files_accepted.pop(send_id, None)
if req is not None:
return (False, req) # <-- Confusion 2: type-agnostic token
if printit:
...
elif not self.file_transfer or self.file_transfer_ask: # FILE flags, used for a URL
return None
if openit and (not self.open_files or self.open_files_ask):
openit = False
return (printit, openit)
So a URL open is authorized by file_transfer/open_files, not by the open_url* policy intended to gate it.
Confusion 2: the auto-accept token is type-agnostic. accept_data() returns the files_accepted[send_id] token before any flag or type check. That token is granted in the context of a file request (do_process_send_data_request matching request-file, file_transfer.py:828), but it is honoured for a URL open as well, meaning a token issued for a file authorizes an arbitrary URL.
Impact
A rogue server can make the client open a fully server-controlled URL, regardless of the open-url configuration. Two paths exist:
- With
file-transfer=yes(file_transfer_ask=False) +open-url=auto, confusion 1 alone makesaccept_data()return a truthy tuple →_open_url(url)runs with no prompt. - In default configuration, confusion 2 is reached zero-click via the same
controlseeding as finding 3:- server sends
control show_bug_reportwhich setsfiles_requested["${XPRA_SERVER_LOG}"]=True. - server sends
send-data-requestechoingoptions["request-file"]=["${XPRA_SERVER_LOG}",1]which seeds setsfiles_accepted[send_id]=True. - server sends
["open-url", "<attacker-url>", send_id]which lands into_process_open_urlbecauseaccept_datapops the (type-agnostic) send_id. The attacker url is then forwarded to_open_url(url)which callsexec_open_command(url)executing the shell commandxdg-open <attacker-url>.
- server sends
This is the same exec_open_command sink as finding 3, reached for a URL without writing a file. xdg-open on an attacker-chosen URL/scheme enables:
http(s), drive-by exploitation, phishing, and client-side SSRF (reaching the victim's localhost / intranet from their browser);file://, opening arbitrary local files in their installed handler;- any registered scheme handler which may exploit a vulnerable application;
- exploiting Xpra's own vulnerable URL handler to gain code execution on the client's host.
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least.
Finding 7: Image and video codec configuration bypass
Description
A remote-display client must decode the pixels the server sends, and the draw packet lets the server choose the codec and supply the entire bitstream. draw_region() dispatches the server-chosen coding straight to a decoder, and the choice depends only on which decoder modules are loaded, never on the encodings the client actually advertised in the encodings and video-decoders configuration options:
# xpra/client/gui/window_backing_base.py:898
def draw_region(self, x, y, width, height, coding, img_data, rowstride, options, callbacks):
...
coding = bytestostr(coding)
if coding == "mmap":
...
elif coding in ("rgb24", "rgb32"):
...
elif coding in VIDEO_DECODERS:
self.paint_with_video_decoder(VIDEO_DECODERS.get(coding), coding, ...) # openh264 / libvpx / ffmpeg
elif self.jpeg_decoder and coding=="jpeg":
self.paint_jpeg(...) # libjpeg-turbo
elif self.avif_decoder and coding=="avif":
self.paint_avif(...) # libavif
elif coding == "webp":
self.paint_webp(...) # libwebp
elif self.spng_decoder and coding=="png":
self.paint_spng(...) # libspng / libpng
elif coding in self._PIL_encodings:
self.paint_image(coding, ...) # Pillow
The same attacker-controlled bytes also reach decoders through window-icon and cursor packets.
The dispatch never checks the server-chosen coding against the encodings the client advertised. For instance the webp branch is reached unconditionally whenever the always-present Pillow library is loaded (coding == "webp" has no self.*_decoder guard). The encodings and video-decoders lists are honoured only during capability negotiation, not when an incoming draw packet is decoded.
Impact
A rogue server can still reach potentially dangerous installed codecs (libwebp, libvpx, ffmpeg, openh264, libjpeg-turbo, libspng, libavif) even when the client hardened its configuration to disable them.
Recommendation
Upgrade Xpra client to version 5.1.6 or 6.5.1 at least. If possible do not install xpra-codecs and xpra-codecs-extra as they populate most of the potentially vulnerable encoders.