diff --git a/webroot/js/inject-iframe.js b/webroot/js/inject-iframe.js index f24ca6a1..357fe0d6 100644 --- a/webroot/js/inject-iframe.js +++ b/webroot/js/inject-iframe.js @@ -10,6 +10,14 @@ if (elem) { let bodyOverflow; const onMessage = (event) => { + // The toolbar iframe is same-origin with the host app; reject any + // postMessage that does not originate from the iframe we created. + if (event.origin !== win.location.origin) { + return; + } + if (!iframe || event.source !== iframe.contentWindow) { + return; + } if (event.data === 'collapse') { iframe.height = 40; iframe.width = 40; diff --git a/webroot/js/modules/Panels/CachePanel.js b/webroot/js/modules/Panels/CachePanel.js index 24050762..bf3fc205 100644 --- a/webroot/js/modules/Panels/CachePanel.js +++ b/webroot/js/modules/Panels/CachePanel.js @@ -1,6 +1,6 @@ export default (($) => { const addMessage = (text) => { - $(`
${text}
`) + $('').text(text) .appendTo('.c-cache-panel__messages') .fadeOut(2000); }; diff --git a/webroot/js/modules/Panels/HistoryPanel.js b/webroot/js/modules/Panels/HistoryPanel.js index 856ffd75..b5969b1b 100644 --- a/webroot/js/modules/Panels/HistoryPanel.js +++ b/webroot/js/modules/Panels/HistoryPanel.js @@ -11,24 +11,32 @@ export default (($) => { for (let i = 0; i < toolbar.ajaxRequests.length; i++) { const element = toolbar.ajaxRequests[i]; - const params = { - id: element.requestId, - time: (new Date(element.date)).toLocaleString(), - method: element.method, - status: element.status, - url: element.url, - type: element.type, - }; - const content = listItem.replace(/{([^{}]*)}/g, (a, b) => { - const r = params[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - }); - $('.c-history-panel__list li:first').after(content); + // Request ID is a server-generated UUID; sanitize defensively before + // substituting into the href/data-request attribute scaffold. + const safeId = String(element.requestId || '').replace(/[^A-Za-z0-9-]/g, ''); + + // Only the {id} placeholder feeds attributes; substitute it now and + // populate the visible text via .text() so attacker-controlled values + // (method/URL/Content-Type from cross-origin responses) cannot inject + // HTML into the toolbar iframe (same-origin with the dev's app). + const $row = $(listItem.replace(/\{id\}/g, safeId)); + $row.find('.c-history-panel__time').text((new Date(element.date)).toLocaleString()); + // bubble[0] is the static "XHR" label; the next three are method/status/type. + const $bubbles = $row.find('.c-history-panel__bubble').not('.c-history-panel__xhr'); + $bubbles.eq(0).text(String(element.method ?? '')); + $bubbles.eq(1).text(String(element.status ?? '')); + $bubbles.eq(2).text(String(element.type ?? '')); + $row.find('.c-history-panel__url').text(String(element.url ?? '')); + + $('.c-history-panel__list li:first').after($row); } const links = $('.c-history-panel__link'); - // Highlight the active request. - links.filter(`[data-request=${toolbar.currentRequest}]`).addClass('is-active'); + // Highlight the active request via attribute comparison rather than an + // unquoted attribute-selector built from a runtime value. + links.filter(function highlightActive() { + return this.getAttribute('data-request') === String(toolbar.currentRequest); + }).addClass('is-active'); links.on('click', function historyLinkClick(e) { const el = $(this); diff --git a/webroot/js/modules/Panels/PackagesPanel.js b/webroot/js/modules/Panels/PackagesPanel.js index 436f4a73..fcd4c557 100644 --- a/webroot/js/modules/Panels/PackagesPanel.js +++ b/webroot/js/modules/Panels/PackagesPanel.js @@ -1,26 +1,35 @@ export default (($) => { const buildSuccessfulMessage = (response) => { - let html = ''; + const $out = $('
All dependencies are up to date'; + $out.append($('
').addClass('c-packages-panel__up2date').text('All dependencies are up to date'));
+ return $out;
}
if (response.packages.bcBreaks !== undefined) {
- html += 'Update with potential BC break
';
- html += `${response.packages.bcBreaks}`;
+ $out.append($('').addClass('c-packages-panel__section-header').text('Update with potential BC break'));
+ $out.append($('').text(String(response.packages.bcBreaks)));
}
if (response.packages.semverCompatible !== undefined) {
- html += 'Update semver compatible
';
- html += `${response.packages.semverCompatible}`;
+ $out.append($('').addClass('c-packages-panel__section-header').text('Update semver compatible'));
+ $out.append($('').text(String(response.packages.semverCompatible)));
}
- return html;
+ return $out;
};
- const showMessage = (el, html) => {
- el.show().html(html);
+ const showMessage = (el, $content) => {
+ el.show().empty().append($content);
$('.o-loader').removeClass('is-loading');
};
- const buildErrorMessage = (response) => ``;
+ const buildErrorMessage = (jqXHR) => {
+ let message = '';
+ try {
+ message = String(JSON.parse(jqXHR.responseText).message ?? '');
+ } catch (_e) {
+ message = String(jqXHR.responseText || jqXHR.statusText || 'Request failed');
+ }
+ return $('').addClass('c-packages-panel__warning-message').text(message);
+ };
const init = () => {
const $panel = $('.c-packages-panel');
@@ -41,8 +50,8 @@ export default (($) => {
success(data) {
showMessage($terminal, buildSuccessfulMessage(data));
},
- error(jqXHR, textStatus) {
- showMessage($terminal, buildErrorMessage(textStatus));
+ error(jqXHR) {
+ showMessage($terminal, buildErrorMessage(jqXHR));
},
});
e.preventDefault();
diff --git a/webroot/js/modules/Panels/RequestPanel.js b/webroot/js/modules/Panels/RequestPanel.js
index 7e5da614..4802b528 100644
--- a/webroot/js/modules/Panels/RequestPanel.js
+++ b/webroot/js/modules/Panels/RequestPanel.js
@@ -1,6 +1,6 @@
export default (($) => {
const addMessage = (text) => {
- $(`${text}
`)
+ $('').text(text)
.appendTo('.c-request-panel__messages')
.fadeOut(2000);
};
diff --git a/webroot/js/modules/Toolbar.js b/webroot/js/modules/Toolbar.js
index c3d9d9c6..e87d31ac 100644
--- a/webroot/js/modules/Toolbar.js
+++ b/webroot/js/modules/Toolbar.js
@@ -236,6 +236,14 @@ export default class Toolbar {
// ========== AJAX related functionality ==========
onMessage(event) {
+ // Only accept messages from the parent window that loaded the toolbar
+ // iframe; the toolbar is served same-origin so origin must match too.
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+ if (event.source !== window.parent) {
+ return;
+ }
if (typeof (event.data) === 'string' && event.data.indexOf('ajax-completed$$') === 0) {
this.onRequest(JSON.parse(event.data.split('$$')[1]));
}