CVE-2025-32428: TigerVNC Exposure in jupyter-remote-desktop-proxy

Executive Summary

CVE-2025-32428 describes a security vulnerability in jupyter-remote-desktop-proxy version 3.0.0. When configured with TigerVNC, the VNC server was unintentionally accessible via the network, rather than solely through a UNIX socket as intended. This could allow unauthorized network access to the VNC server. The vulnerability is resolved in version 3.0.1. Users employing TurboVNC as their vncserver executable are not affected.

Technical Details

  • CVE ID: CVE-2025-32428
  • Affected Software: jupyter-remote-desktop-proxy
  • Affected Version: 3.0.0
  • Fixed Version: 3.0.1
  • Vulnerability Type: Unintended Network Exposure
  • Affected Component: VNC server configuration
  • Attack Vector: Network
  • Impact: Unauthorized access to the remote desktop environment.

The jupyter-remote-desktop-proxy package is designed to provide a secure way to access remote desktop environments within a JupyterHub environment. Version 3.0.0 aimed to restrict VNC server access to UNIX sockets, enhancing security by preventing direct network connections. However, a configuration issue arose when using TigerVNC as the VNC server, causing it to remain accessible via TCP. This bypasses the intended security measure, potentially exposing the remote desktop to unauthorized network access.

Root Cause Analysis

The root cause of CVE-2025-32428 lies in the way jupyter-remote-desktop-proxy configures the VNC server. The intended configuration relies on the -rfbunixpath argument to force VNC connections through a UNIX socket. However, TigerVNC, by default, also listens on a TCP port (typically 5900 + display number) unless explicitly told not to.

The vulnerability existed because the jupyter-remote-desktop-proxy did not explicitly disable the TCP port when using TigerVNC. The code responsible for setting up the VNC server arguments is located in jupyter_remote_desktop_proxy/setup_websockify.py.

Prior to the fix, the relevant part of the code looked like this:

vnc_args = [vncserver, '-rfbunixpath', '{unix_socket}']

This code only specified the UNIX socket path but did not prevent TigerVNC from also listening on a TCP port.

The fix addresses this by explicitly adding the -rfbport -1 argument to the vnc_args list when TigerVNC is detected. This argument tells TigerVNC not to listen on any TCP port.

The code now includes logic to detect whether TigerVNC or TurboVNC is being used. This is important because TurboVNC does not require the -rfbport -1 argument to disable TCP connections; it defaults to only using UNIX sockets. Passing -rfbport -1 to TurboVNC is also not supported.

The detection logic involves reading the vncserver executable file and checking for the string "turbovnc". This is based on the observation that both TigerVNC and TurboVNC use a Perl script as the vncserver executable, and the contents of this script differ depending on whether it is TigerVNC or TurboVNC.

Patch Analysis

The primary fix for CVE-2025-32428 is implemented in the jupyter_remote_desktop_proxy/setup_websockify.py file. The patch ensures that TigerVNC is not accessible via the network by explicitly disabling the TCP port.

File: jupyter_remote_desktop_proxy/setup_websockify.py
--- a/jupyter_remote_desktop_proxy/setup_websockify.py
+++ b/jupyter_remote_desktop_proxy/setup_websockify.py
@@ -12,8 +12,27 @@ def setup_websockify():
             "vncserver executable not found, please install a VNC server"
         )
 
+    # TurboVNC and TigerVNC share the same origin and both use a Perl script
+    # as the executable vncserver. We can determine if vncserver is TigerVNC
+    # by searching tigervnc string in the Perl script.
+    #
+    # The content of the vncserver executable can differ depending on how
+    # TigerVNC and TurboVNC has been distributed. Below are files known to be
+    # read in some situations:
+    #
+    # - https://github.com/TigerVNC/tigervnc/blob/v1.13.1/unix/vncserver/vncserver.in
+    # - https://github.com/TurboVNC/turbovnc/blob/3.1.1/unix/vncserver.in
+    #
+    with open(vncserver) as vncserver_file:
+        vncserver_file_text = vncserver_file.read().casefold()
+    is_turbovnc = "turbovnc" in vncserver_file_text
+
     # {unix_socket} is expanded by jupyter-server-proxy
-    vnc_args = [vncserver, '-rfbunixpath', '{unix_socket}']
+    vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}", "-rfbport", "-1"]
+    if is_turbovnc:
+        # turbovnc doesn't handle being passed -rfbport -1, but turbovnc also
+        # defaults to not opening a TCP port which is what we want to ensure
+        vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}"]
 
     xstartup = os.getenv("JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP")
     if not xstartup and not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):

Explanation:

  1. TigerVNC/TurboVNC Detection: The code first determines whether TigerVNC or TurboVNC is being used. It reads the contents of the vncserver executable and checks if the string "turbovnc" is present. This is done in a case-insensitive manner using .casefold().
  2. Conditional Argument Addition: If TigerVNC is detected, the -rfbport -1 argument is added to the vnc_args list. This disables the TCP port.
  3. TurboVNC Handling: If TurboVNC is detected, the -rfbport -1 argument is not added. This is because TurboVNC defaults to using UNIX sockets and does not support the -rfbport -1 argument.

This patch effectively addresses the vulnerability by ensuring that TigerVNC does not listen on a TCP port when used with jupyter-remote-desktop-proxy, enforcing the intended security model of UNIX socket-only access.

Additionally, the following changes were made to the testing infrastructure to verify the fix:

File: .github/workflows/test.yaml
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -60,7 +60,13 @@ jobs:
 
       - name: Test vncserver
         run: |\
-          container_id=$(docker run -d -it test vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbunixpath /tmp/vncserver.socket)
+          # TigerVNC needs to be configured with -rfbport -1 to not open a TCP
+          # port, while TurboVNC doesn't support being passed -1 and won't open
+          # a TCP port anyhow.
+          rfbport_arg="-rfbport -1"
+          if [ "${{ matrix.vncserver }}" == "turbovnc" ]; then rfbport_arg=""; fi
+
+          container_id=$(docker run -d -it test vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbunixpath /tmp/vncserver.socket $rfbport_arg)
           sleep 1
 
           echo "::group::Install netcat, a test dependency"
@@ -70,9 +76,18 @@ jobs:
           '
           echo "::endgroup::"
 
-          docker exec -it $container_id timeout --preserve-status 1 nc -vU /tmp/vncserver.socket 2>&1 | tee -a /dev/stderr | \
+          docker exec -it $container_id timeout --preserve-status 1 nc -vU /tmp/vncserver.socket 2>&1 | tee -a /dev/stderr | \
               grep --quiet RFB && echo "Passed test" || { echo "Failed test" && TEST_OK=false; }
 
+          echo "::group::Security - Verify TCP ports wasn't opened"
+          ports=(5800 5801 5900 5901)
+          for port in "${ports[@]}"
+          do
+              docker exec -it $container_id timeout --preserve-status 1 nc -vz localhost $port | tee -a /dev/stderr | \
+                  grep --quiet succeeded && { echo "Failed security check - port $port open" && SECURITY_OK=false; } || echo "Passed security check - port $port not opened"
+          done
+          echo "::endgroup::"
+
           echo "::group::vncserver logs"
           docker exec $container_id bash -c 'cat ~/.vnc/*.log'
           echo "::endgroup::"
@@ -82,6 +97,10 @@ jobs:
               echo "Test failed!"
               exit 1
           fi
+          if [ "$SECURITY_OK" == "false" ]; then
+              echo "Security check failed!"
+              exit 1
+          fi
 
       - name: Install playwright
         run: |

Explanation:

  1. Conditional rfbport Argument: The test script now conditionally adds the -rfbport -1 argument based on whether TigerVNC or TurboVNC is being used, mirroring the logic in the patch.
  2. TCP Port Verification: A new security check is added to verify that TCP ports are not open. This check uses netcat (nc) to attempt to connect to ports 5800, 5801, 5900, and 5901 on localhost. If any of these connections succeed, the security check fails, indicating that the VNC server is listening on a TCP port.

These changes to the testing infrastructure ensure that the fix is effective and that the VNC server is only accessible via the UNIX socket.

Exploitation Techniques

An attacker could exploit this vulnerability by directly connecting to the VNC server's TCP port (typically 5900 + display number) if TigerVNC is in use. This would bypass the intended UNIX socket restriction, granting unauthorized access to the remote desktop environment.

Attack Scenario:

  1. Discovery: The attacker scans the network for open VNC ports (5900-5905 are common).
  2. Identification: The attacker identifies a system running jupyter-remote-desktop-proxy with TigerVNC.
  3. Connection: The attacker uses a VNC client to connect to the open TCP port.
  4. Authentication: If the VNC server is not configured with a password, the attacker gains immediate access. If a password is set, the attacker may attempt to brute-force or exploit known vulnerabilities in the VNC authentication mechanism.
  5. Access: Upon successful authentication (or lack thereof), the attacker gains control of the remote desktop environment.

Proof of Concept (Made-up):

This is a theoretical proof of concept, as exploiting the vulnerability requires a vulnerable environment.

  1. Setup: Install jupyter-remote-desktop-proxy version 3.0.0 with TigerVNC.

  2. Port Scan: Use nmap to scan for open VNC ports:

    nmap -p 5900-5905 <target_ip>
    
  3. VNC Connection: If a port is open, connect using a VNC client:

    vncviewer <target_ip>:5901
    

    (Replace 5901 with the actual open port).

Real-World Impact:

Successful exploitation could lead to:

  • Data theft: Access to sensitive data stored on the remote desktop.
  • Malware installation: Installation of malware on the compromised system.
  • Lateral movement: Use of the compromised system to attack other systems on the network.
  • Denial of service: Disrupting the availability of the remote desktop environment.

Mitigation Strategies

To mitigate CVE-2025-32428, the following steps are recommended:

  1. Upgrade: Upgrade jupyter-remote-desktop-proxy to version 3.0.1 or later. This version contains the fix for the vulnerability.

    pip install --upgrade jupyter-remote-desktop-proxy
    
  2. Verify Configuration: Ensure that the VNC server is configured to only listen on a UNIX socket and not on a TCP port. This can be verified by checking the VNC server's command-line arguments and logs.

  3. Firewall: Configure a firewall to block access to the VNC server's TCP port from unauthorized networks.

  4. VNC Password: Always configure a strong password for the VNC server.

  5. Use TurboVNC: If possible, use TurboVNC as the vncserver executable. TurboVNC defaults to using UNIX sockets and is not affected by this vulnerability.

  6. Security Best Practices: Follow general security best practices, such as keeping software up to date, using strong passwords, and monitoring network traffic for suspicious activity.

Timeline of Discovery and Disclosure

  • Vulnerability Discovered: Unknown
  • Vulnerability Reported: Unknown
  • Patch Released: 2025-04-11
  • Public Disclosure: 2025-04-12

References

Read more