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 = $('

'); if (response.packages.bcBreaks === undefined && response.packages.semverCompatible === undefined) { - return '
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) => `
${JSON.parse(response.responseText).message}
`; + 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])); }