From f19c30840eb42326c7db3c896f63442df7a31fb6 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 26 Sep 2026 19:28:22 +0300 Subject: [PATCH] fix(console): prevent DOM-based XSS in console views and escape JS strings - Validate and coerce console query parameters (view_only, scale, resize_session, clip_viewport) to strict booleans via to_bool in console/views.py - Sanitize view parameter to strictly 'lite' or 'full' - Apply escapejs filter to all JS string interpolations in console-vnc-full.html, console-vnc-lite.html, console-spice-lite.html, and console-xterm.html - Replace unsafe '| safe' filter in console-spice-lite.html with escapejs - Add unit tests for XSS query param mitigation and escapejs character encoding --- console/templates/console-spice-lite.html | 11 +++-- console/templates/console-vnc-full.html | 14 +++---- console/templates/console-vnc-lite.html | 20 ++++----- console/templates/console-xterm.html | 6 +-- console/tests.py | 50 +++++++++++++++++++++++ console/views.py | 36 ++++++++++++---- 6 files changed, 102 insertions(+), 35 deletions(-) diff --git a/console/templates/console-spice-lite.html b/console/templates/console-spice-lite.html index 469c9715..d6ddf3df 100644 --- a/console/templates/console-spice-lite.html +++ b/console/templates/console-spice-lite.html @@ -65,7 +65,7 @@ // By default, use the host and port of server that served this file // host = spice_query_var('host', window.location.hostname); - host = '{{ ws_host| safe }}'; + host = '{{ ws_host|escapejs }}'; // Note that using the web server port only makes sense // if your web server has a reverse proxy to relay the WebSocket @@ -80,7 +80,7 @@ } } //port = spice_query_var('port', default_port); - port = '{{ ws_port| safe }}'; + port = '{{ ws_port|escapejs }}'; if (window.location.protocol == 'https:') { scheme = "wss://"; } @@ -93,17 +93,16 @@ } if (password === undefined) { - {% if perms.instances.passwordless_console %} - password = '{{ console_passwd }}'; + {% if perms.instances.passwordless_console and console_passwd %} + password = '{{ console_passwd|escapejs }}'; {% else %} password = prompt('{% trans "Password" %}'); {% endif %} - //password = '{{ console_passwd | safe }}'; } if (password === 'None') password = ''; //var path = spice_query_var('path', 'websockify'); - var path = spice_query_var('path', '{{ ws_path }}'); + var path = spice_query_var('path', '{{ ws_path|escapejs }}'); if ((!host) || (!port)) { console.log('{% trans "must specify host and port in URL" %}'); diff --git a/console/templates/console-vnc-full.html b/console/templates/console-vnc-full.html index 714ab036..aedeb2b5 100755 --- a/console/templates/console-vnc-full.html +++ b/console/templates/console-vnc-full.html @@ -176,24 +176,24 @@ bindKeyAction('ctrlaltf' + (i + 1), () => sendCtrlAltFN(i)); } - let fullPath = '{{ ws_path }}'; - const token = '{{ token }}'; + let fullPath = '{{ ws_path|escapejs }}'; + const token = '{{ token|escapejs }}'; if (token) { fullPath += (fullPath.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); } const defaults = { autoconnect: true, - resize: '{{ scale }}' === 'true' ? 'scale' : 'off', - view_only: {{ view_only }}, - view_clip: {{ clip_viewport }}, + resize: {{ scale|yesno:"'scale','off'" }}, + view_only: {{ view_only|yesno:"true,false" }}, + view_clip: {{ clip_viewport|yesno:"true,false" }}, {% if perms.instances.passwordless_console and console_passwd %} - password: '{{ console_passwd }}', + password: '{{ console_passwd|escapejs }}', {% endif %} }; const mandatory = { - host: '{{ ws_host }}', + host: '{{ ws_host|escapejs }}', port: {{ ws_port }}, path: fullPath, }; diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html index d4d0c063..0fbeb745 100755 --- a/console/templates/console-vnc-lite.html +++ b/console/templates/console-vnc-lite.html @@ -260,17 +260,17 @@ // Read parameters specified in the URL query string // By default, use the host and port of server that served this file - const host = readQueryVariable('host', '{{ ws_host }}'); - let port = readQueryVariable('port', '{{ ws_port }}'); + const host = readQueryVariable('host', '{{ ws_host|escapejs }}'); + let port = readQueryVariable('port', '{{ ws_port|escapejs }}'); - {% if perms.instances.passwordless_console %} - const password = '{{ console_passwd }}'; + {% if perms.instances.passwordless_console and console_passwd %} + const password = '{{ console_passwd|escapejs }}'; {% else %} const password = readQueryVariable('password'); {% endif %} //const path = readQueryVariable('path', 'websockify'); - const path = readQueryVariable('path', '{{ ws_path }}'); + const path = readQueryVariable('path', '{{ ws_path|escapejs }}'); // | | | | | | // | | | Connect | | | @@ -294,7 +294,7 @@ } else { url += '/'; } - const token = readQueryVariable('token', '{{ token }}'); + const token = readQueryVariable('token', '{{ token|escapejs }}'); if (token) { url += (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); } @@ -310,10 +310,10 @@ rfb.addEventListener("desktopname", updateDesktopName); // Set parameters that can be changed on an active connection - rfb.scaleViewport = {{ scale }}; - rfb.viewOnly = {{ view_only }}; - rfb.resizeSession = {{ resize_session }}; - rfb.clipViewport = {{ clip_viewport }}; + rfb.scaleViewport = {{ scale|yesno:"true,false" }}; + rfb.viewOnly = {{ view_only|yesno:"true,false" }}; + rfb.resizeSession = {{ resize_session|yesno:"true,false" }}; + rfb.clipViewport = {{ clip_viewport|yesno:"true,false" }}; {% endblock %} diff --git a/console/templates/console-xterm.html b/console/templates/console-xterm.html index 9fd1a853..3bae98f1 100644 --- a/console/templates/console-xterm.html +++ b/console/templates/console-xterm.html @@ -22,9 +22,9 @@