From b393e2e3a30745cc507e9a19c6ee9aef8d23f091 Mon Sep 17 00:00:00 2001
From: Ioannis Dressos <96877388+idressos@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:22:34 +0300
Subject: [PATCH] Escape user-controlled fields to prevent stored XSS in
client/status views
---
custom/js/helper.js | 64 +++++++++++++++++++++++++++++----------------
handler/routes.go | 7 +++--
2 files changed, 46 insertions(+), 25 deletions(-)
diff --git a/custom/js/helper.js b/custom/js/helper.js
index 5b43272..c21938a 100644
--- a/custom/js/helper.js
+++ b/custom/js/helper.js
@@ -1,18 +1,35 @@
+// escapeHtml escapes characters that are significant in HTML so that
+// user-controlled values (client names, emails, notes, ...) cannot be used to
+// inject markup or scripts when interpolated into the DOM.
+function escapeHtml(value) {
+ if (value === null || value === undefined) {
+ return '';
+ }
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
function renderClientList(data) {
$.each(data, function(index, obj) {
+ const clientName = escapeHtml(obj.Client.name);
+
// render telegram button
let telegramButton = ''
if (obj.Client.telegram_userid) {
- telegramButton = `
+ telegramButton = `
+ data-clientname="${clientName}">Telegram
`
}
let telegramHtml = "";
if (obj.Client.telegram_userid && obj.Client.telegram_userid.length > 0) {
- telegramHtml = `
${obj.Client.telegram_userid}`
+ telegramHtml = `
${escapeHtml(obj.Client.telegram_userid)}`
}
// render client status css tag style
@@ -24,23 +41,23 @@ function renderClientList(data) {
// render client allocated ip addresses
let allocatedIpsHtml = "";
$.each(obj.Client.allocated_ips, function(index, obj) {
- allocatedIpsHtml += `
${obj} `;
+ allocatedIpsHtml += `
${escapeHtml(obj)} `;
})
// render client allowed ip addresses
let allowedIpsHtml = "";
$.each(obj.Client.allowed_ips, function(index, obj) {
- allowedIpsHtml += `
${obj} `;
+ allowedIpsHtml += `
${escapeHtml(obj)} `;
})
let subnetRangesString = "";
if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) {
- subnetRangesString = obj.Client.subnet_ranges.join(',')
+ subnetRangesString = escapeHtml(obj.Client.subnet_ranges.join(','))
}
let additionalNotesHtml = "";
if (obj.Client.additional_notes && obj.Client.additional_notes.length > 0) {
- additionalNotesHtml = `
${obj.Client.additional_notes.toUpperCase()}`
+ additionalNotesHtml = `
${escapeHtml(obj.Client.additional_notes.toUpperCase())}`
}
// render client html content
@@ -53,41 +70,41 @@ function renderClientList(data) {
-
+
+ data-clientname="${clientName}" ${obj.QRCode != "" ? '' : ' disabled'}>QR code
-
+
+ data-clientname="${clientName}">Email
${telegramButton}
-
-
${obj.Client.name}
-
${obj.Client.public_key}
+
${clientName}
+
${escapeHtml(obj.Client.public_key)}
${subnetRangesString}
${telegramHtml}
${additionalNotesHtml}
-
${obj.Client.email}
+
${escapeHtml(obj.Client.email)}
${prettyDateTime(obj.Client.created_at)}
@@ -95,7 +112,7 @@ function renderClientList(data) {
${obj.Client.use_server_dns ? 'DNS enabled' : 'DNS disabled'}
- ${obj.Client.additional_notes}
+ ${escapeHtml(obj.Client.additional_notes)}
IP Allocation`
+ allocatedIpsHtml
+ `
Allowed IPs`
@@ -112,20 +129,21 @@ function renderClientList(data) {
function renderUserList(data) {
$.each(data, function(index, obj) {
let clientStatusHtml = '>'
+ const username = escapeHtml(obj.username);
// render user html content
- let html = `
+ let html = `
-
+
+ data-target="#modal_remove_user" data-username="${username}">Delete
-
${obj.username}
+
${username}
${obj.admin? 'Administrator':'Manager'}
diff --git a/handler/routes.go b/handler/routes.go
index de25f74..51127c3 100644
--- a/handler/routes.go
+++ b/handler/routes.go
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "html"
"io/fs"
"net/http"
"os"
@@ -1082,8 +1083,10 @@ func Status(db store.IStore) echo.HandlerFunc {
}
if _client, ok := m[pVm.PublicKey]; ok {
- pVm.Name = _client.Name
- pVm.Email = _client.Email
+ // escape user-controlled fields: the status page is rendered
+ // with text/template, which does not auto-escape HTML
+ pVm.Name = html.EscapeString(_client.Name)
+ pVm.Email = html.EscapeString(_client.Email)
}
devVm.Peers = append(devVm.Peers, pVm)
}