diff --git a/public/app.js b/public/app.js
index 6ca80cd0..e5677a3f 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1281,7 +1281,7 @@ function navigate() {
else { app.setAttribute('tabindex', '-1'); app.focus({ preventScroll: true }); }
});
} else {
- app.innerHTML = `
${route}
Page not yet implemented.
`;
+ app.innerHTML = `${escapeHtml(route)}
Page not yet implemented.
`;
}
}
diff --git a/tests/unit/test-xss-escape-sinks.js b/tests/unit/test-xss-escape-sinks.js
index 350927cc..5867f9ee 100644
--- a/tests/unit/test-xss-escape-sinks.js
+++ b/tests/unit/test-xss-escape-sinks.js
@@ -588,6 +588,18 @@ test('loadDetail catch block does NOT interpolate e.message into innerHTML', ()
'loadDetail missing textContent assignment for e.message: ' + body);
});
+// =========================================================================
+// E. URL-derived strings
+// =========================================================================
+console.log('\n=== E. URL-derived strings ===');
+
+test('app.js unknown-route page escapes the route taken from location.hash', () => {
+ const html = evalTemplate('public/app.js',
+ /(\$\{[^}]*route[^}]*\}<\/h2>
Page not yet implemented\.<\/p><\/div>)/,
+ { route: TAG_PAYLOAD + ATTR_PAYLOAD });
+ assertNoXss(html, 'app.js unknown route');
+});
+
// =========================================================================
// SUMMARY
// =========================================================================