@@ -2223,7 +2236,14 @@ function vvToggleContainer(name) {
}
function vvDockerAction(action, name, webui) {
- if (action === 'webui') { window.open(webui, '_blank'); return; }
+ // Filtered again at the point of use, not only where the button was built. This value originates
+ // in a container's template XML, and window.open() on a javascript: URL runs it with this page's
+ // origin — the one sink where an unchecked scheme is not merely a broken link.
+ if (action === 'webui') {
+ const u = vvSafeUrl(webui);
+ if (u) window.open(u, '_blank', 'noopener');
+ return;
+ }
if (action === 'edit') {
window.location.href = '/Docker?action=template&xmlTemplate=' +
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
@@ -2270,7 +2290,7 @@ function vvRenderDockerFolders(data) {
html += `
${osIcon(vm.os)}
- ${vm.name}
+ ${vvEscHtml(vm.name)}
${stateLabel(vm.state)}
${meta}
`;
@@ -2288,8 +2308,14 @@ function vvRenderDockerFolders(data) {
const pulse = c.running ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
const active = vvDfActive === c.name;
const sShort = c.status ? c.status.replace(/^Up\s+/, '').split(' ').slice(0,2).join(' ') : '—';
- const sn = c.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
- const sw = (c.webui||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
+ // Escaped for two nested contexts at once: a JS string literal, and the double-quoted onclick
+ // attribute holding it. The previous version did the first half only — \ and ' — which leaves
+ // a " free to close the attribute and destroy every handler after it. Docker's own charset
+ // makes that unreachable through a container name, but the WebUI value comes from template
+ // XML and is under no such constraint.
+ const jsq = v => vvEscAttr(String(v ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
+ const sn = jsq(c.name);
+ const sw = jsq(vvSafeUrl(c.webui));
let actionBar = '';
if (active) {
@@ -2309,8 +2335,8 @@ function vvRenderDockerFolders(data) {
return `
- ${c.name}
- ${sShort}
+ ${vvEscHtml(c.name)}
+ ${vvEscHtml(sShort)}
${actionBar}`;
}
@@ -2320,20 +2346,20 @@ function vvRenderDockerFolders(data) {
const running = f.containers.filter(c => c.running).length;
const bColor = running === total ? '#4caf50' : running === 0 ? '#555' : '#ff9800';
const badge = `
${running}/${total}`;
- const sid = f.id.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
+ const sid = vvEscAttr(String(f.id ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
let iconHtml = '';
if (f.isEmoji) {
- iconHtml = `
${f.icon}`;
- } else if (f.icon) {
- iconHtml = `

`;
+ iconHtml = `
${vvEscHtml(f.icon)}`;
+ } else if (vvSafeUrl(f.icon)) {
+ iconHtml = `
)})
`;
}
let out = `
${open ? '▾' : '▸'}
${iconHtml}
- ${f.name}
+ ${vvEscHtml(f.name)}
${badge}
`;
if (open) {
diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php
index 6513cbc..9821853 100644
--- a/Plugin/unraid/pages/scheduler.php
+++ b/Plugin/unraid/pages/scheduler.php
@@ -1158,18 +1158,10 @@ if (vvOpenScriptId) {
});
}
-function vvEscHtml(s) {
- return s.replace(/&/g,'&').replace(//g,'>');
-}
-
-// For text going INSIDE a double-quoted attribute, which vvEscHtml does not cover: it leaves "
-// alone, and a quote there ends the attribute early and silently destroys the handler after it.
-// That is not a theoretical hazard — it shipped, and an onclick built from JSON.stringify() output
-// was truncated to "vvErrOpenAtLine(" and did nothing at all when clicked.
-function vvEscAttr(s) {
- return String(s).replace(/&/g,'&').replace(/"/g,'"')
- .replace(//g,'>');
-}
+// vvEscHtml() and vvEscAttr() moved to Varaverk.page, which every tab loads — they were needed on
+// pages that never include this one. The attribute variant exists because a " inside a
+// double-quoted attribute ends it early and silently destroys the handler after it: that shipped
+// once, truncating an onclick to "vvErrOpenAtLine(" so it did nothing at all when clicked.
function vvPost(url, data) {
const params = new URLSearchParams({csrf_token, ...data});