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 = `` + telegramHtml = `` } // 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 = `` + additionalNotesHtml = `` } // render client html content @@ -53,41 +70,41 @@ function renderClientList(data) {
Download
-
+
+ data-clientname="${clientName}" ${obj.QRCode != "" ? '' : ' disabled'}>QR code
-
+
+ data-clientname="${clientName}">Email
${telegramButton}
-

- ${obj.Client.name} - + ${clientName} + ${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) }