From 89f8c8432eddc0af522cc06c88048f186a2ce17d Mon Sep 17 00:00:00 2001 From: diosmosis Date: Mon, 17 Sep 2018 19:48:28 -0700 Subject: [PATCH 1/6] Use postMessage instead of directly making API calls in the overlay iframe. --- plugins/Overlay/client/client.js | 66 ++++++++++++++++---- plugins/Overlay/javascripts/Piwik_Overlay.js | 46 +++++++++++++- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/plugins/Overlay/client/client.js b/plugins/Overlay/client/client.js index 9ae78cf3b1d..580eb90fe52 100644 --- a/plugins/Overlay/client/client.js +++ b/plugins/Overlay/client/client.js @@ -15,12 +15,18 @@ var Piwik_Overlay_Client = (function () { /** Reference to the status bar DOM element */ var statusBar; + /** Counter for request IDs for postMessage based API requests. */ + var lastRequestId = 0; + + /** Map of callbacks for postMessage based API requests. */ + var requestCallbacks = {}; + /** Load the client CSS */ function loadCss() { var css = c('link').attr({ rel: 'stylesheet', type: 'text/css', - href: piwikRoot + 'plugins/Overlay/client/client.css' + href: piwikRoot + '/plugins/Overlay/client/client.css' }); $('head').append(css); } @@ -51,7 +57,7 @@ var Piwik_Overlay_Client = (function () { // check whether the session has been opened in a new tab (instead of an iframe) if (window != window.top) { var iframe = c('iframe', false, { - src: piwikRoot + 'index.php?module=Overlay&action=notifyParentIframe#' + window.location.href + src: piwikRoot + '/index.php?module=Overlay&action=notifyParentIframe#' + window.location.href }).css({width: 0, height: 0, border: 0}); $('body').append(iframe); @@ -81,11 +87,47 @@ var Piwik_Overlay_Client = (function () { return el; } + function nextRequestId() { + var nextId = lastRequestId + 1; + lastRequestId = nextId; + return nextId; + } + + function handlePostMessages() { + window.addEventListener("message", function (event) { + if (event.origin !== piwikRoot) { + return; + } + + var strData = event.data.split(':', 3); + if (strData[0] !== 'overlay.response') { + return; + } + + var requestId = strData[1]; + if (!requestCallbacks[requestId]) { + return; + } + + var callback = requestCallbacks[requestId]; + delete requestCallbacks[requestId]; + + var data = JSON.parse(decodeURIComponent(strData[2])); + if (typeof data.result !== 'undefined' + && data.result === 'error' + ) { + alert('Error: ' + data.message); + } else { + callback(data); + } + }, false); + } + return { /** Initialize in-site analytics */ initialize: function (pPiwikRoot, pIdSite, pPeriod, pDate, pSegment) { - piwikRoot = pPiwikRoot; + piwikRoot = pPiwikRoot.replace(/[\/]+$/, ''); idSite = pIdSite; period = pPeriod; date = pDate; @@ -95,6 +137,7 @@ var Piwik_Overlay_Client = (function () { var loading = this.loadingNotification; loadJQuery(function () { + handlePostMessages(); notifyPiwikOfLocation(); loadCss(); @@ -138,13 +181,13 @@ var Piwik_Overlay_Client = (function () { }; script.onload = onLoad; - script.src = piwikRoot + relativePath + '?v=1'; + script.src = piwikRoot + '/' + relativePath + '?v=1'; head.appendChild(script); }, /** Piwik Overlay API Request */ api: function (method, callback, additionalParams) { - var url = piwikRoot + 'index.php?module=API&method=Overlay.' + method + var url = piwikRoot + '/index.php?module=API&method=Overlay.' + method + '&idSite=' + idSite + '&period=' + period + '&date=' + date + '&format=JSON&filter_limit=-1'; if (segment) { @@ -155,14 +198,11 @@ var Piwik_Overlay_Client = (function () { url += '&' + additionalParams; } - $.getJSON(url + "&jsoncallback=?", function (data) { - if (typeof data.result != 'undefined' && data.result == 'error') { - alert('Error: ' + data.message); - } - else { - callback(data); - } - }); + var requestId = nextRequestId(); + requestCallbacks[requestId] = callback; + + var matomoFrame = window.parent; + matomoFrame.postMessage('overlay.call:' + requestId + ':' + encodeURIComponent(url), piwikRoot); }, /** diff --git a/plugins/Overlay/javascripts/Piwik_Overlay.js b/plugins/Overlay/javascripts/Piwik_Overlay.js index afcbc268ad7..23b761f8fda 100644 --- a/plugins/Overlay/javascripts/Piwik_Overlay.js +++ b/plugins/Overlay/javascripts/Piwik_Overlay.js @@ -7,6 +7,8 @@ var Piwik_Overlay = (function () { + var DOMAIN_PARSE_REGEX = /http(s)?:\/\/(www\.)?([^\/]*)/i; + var $body, $iframe, $sidebar, $main, $location, $loading, $errorNotLoading; var $rowEvolutionLink, $transitionsLink, $visitorLogLink; @@ -18,6 +20,7 @@ var Piwik_Overlay = (function () { var iframeCurrentPageNormalized = ''; var iframeCurrentActionLabel = ''; var updateComesFromInsideFrame = false; + var iframeOrigin = ''; /** Load the sidebar for a url */ function loadSidebar(currentUrl) { @@ -26,7 +29,9 @@ var Piwik_Overlay = (function () { $location.html(' ').unbind('mouseenter').unbind('mouseleave'); iframeCurrentPage = currentUrl; - iframeDomain = currentUrl.match(/http(s)?:\/\/(www\.)?([^\/]*)/i)[3]; + var m = currentUrl.match(DOMAIN_PARSE_REGEX); + iframeDomain = m[3]; + iframeOrigin = m[0]; var params = { module: 'Overlay', @@ -139,6 +144,7 @@ var Piwik_Overlay = (function () { function hashChangeCallback(urlHash) { var location = getOverlayLocationFromHash(urlHash); location = Overlay_Helper.decodeFrameUrl(location); + iframeOrigin = location.match(DOMAIN_PARSE_REGEX)[0]; if (location == iframeCurrentPageNormalized) { return; @@ -158,6 +164,41 @@ var Piwik_Overlay = (function () { updateComesFromInsideFrame = false; } + function handleApiRequests() { + window.addEventListener("message", function (event) { + if (event.origin !== iframeOrigin) { + return; + } + + var strData = event.data.split(':', 3); + if (strData[0] !== 'overlay.call') { + return; + } + + var requestId = strData[1]; + var url = decodeURIComponent(strData[2]); + + var params = broadcast.getValuesFromUrl(url); + + angular.element(document).injector().invoke(['piwikApi', function (piwikApi) { + piwikApi.fetch(params) + .then(function (response) { + sendResponse(response); + }).catch(function (err) { + sendResponse({ + result: 'error', + message: err.message, + }); + }); + }]); + + function sendResponse(data) { + var message = 'overlay.response:' + requestId + ':' + encodeURIComponent(JSON.stringify(data)); + $iframe[0].contentWindow.postMessage(message, iframeOrigin); + } + }, false); + } + return { /** This method is called when Overlay loads */ @@ -181,7 +222,6 @@ var Piwik_Overlay = (function () { $visitorLogLink = $('#overlaySegmentedVisitorLog'); adjustDimensions(); - showLoading(); // apply initial dimensions @@ -206,6 +246,8 @@ var Piwik_Overlay = (function () { hashChangeCallback(''); } + handleApiRequests(); + // handle date selection var $select = $('select#overlayDateRangeSelect').change(function () { var parts = $(this).val().split(';'); From 861de5a5334e2fb55c67b1dfcf503c9906a99f02 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Mon, 17 Sep 2018 20:06:14 -0700 Subject: [PATCH 2/6] Make sure it will work when Matomo is on a subfolder. --- plugins/Overlay/client/client.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/Overlay/client/client.js b/plugins/Overlay/client/client.js index 580eb90fe52..66f20ed1644 100644 --- a/plugins/Overlay/client/client.js +++ b/plugins/Overlay/client/client.js @@ -1,11 +1,16 @@ var Piwik_Overlay_Client = (function () { + var DOMAIN_PARSE_REGEX = /http(s)?:\/\/(www\.)?([^\/]*)/i; + /** jQuery */ var $; /** Url of the Piwik root */ var piwikRoot; + /** protocol and domain of Piwik root */ + var piwikOrigin; + /** Piwik idsite */ var idSite; @@ -95,7 +100,7 @@ var Piwik_Overlay_Client = (function () { function handlePostMessages() { window.addEventListener("message", function (event) { - if (event.origin !== piwikRoot) { + if (event.origin !== piwikOrigin) { return; } @@ -127,7 +132,8 @@ var Piwik_Overlay_Client = (function () { /** Initialize in-site analytics */ initialize: function (pPiwikRoot, pIdSite, pPeriod, pDate, pSegment) { - piwikRoot = pPiwikRoot.replace(/[\/]+$/, ''); + piwikRoot = pPiwikRoot; + piwikOrigin = piwikRoot.match(DOMAIN_PARSE_REGEX)[0]; idSite = pIdSite; period = pPeriod; date = pDate; @@ -202,7 +208,7 @@ var Piwik_Overlay_Client = (function () { requestCallbacks[requestId] = callback; var matomoFrame = window.parent; - matomoFrame.postMessage('overlay.call:' + requestId + ':' + encodeURIComponent(url), piwikRoot); + matomoFrame.postMessage('overlay.call:' + requestId + ':' + encodeURIComponent(url), piwikOrigin); }, /** From d9afeb3cf167becd6149a1f4684c03bc5b7f3665 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Mon, 24 Sep 2018 17:26:44 -0700 Subject: [PATCH 3/6] Increase overlay security with domain and method whitelists. --- plugins/AnonymousPiwikUsageMeasurement | 2 +- plugins/Overlay/Controller.php | 2 + plugins/Overlay/client/client.js | 2 +- plugins/Overlay/javascripts/Piwik_Overlay.js | 46 +++++++++++++++++--- plugins/Overlay/templates/index.twig | 2 + 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/plugins/AnonymousPiwikUsageMeasurement b/plugins/AnonymousPiwikUsageMeasurement index dd87ad77a68..f103f432590 160000 --- a/plugins/AnonymousPiwikUsageMeasurement +++ b/plugins/AnonymousPiwikUsageMeasurement @@ -1 +1 @@ -Subproject commit dd87ad77a68644a3c7cba8dfcc00d79aa31c1909 +Subproject commit f103f432590ebfda9349be1997c991d3e28d3e67 diff --git a/plugins/Overlay/Controller.php b/plugins/Overlay/Controller.php index 8b8b5b6df75..c16d8923a73 100644 --- a/plugins/Overlay/Controller.php +++ b/plugins/Overlay/Controller.php @@ -23,6 +23,7 @@ use Piwik\Tracker\Action; use Piwik\Tracker\PageUrl; use Piwik\View; +use Piwik\Plugins\SitesManager; class Controller extends \Piwik\Plugin\Controller { @@ -53,6 +54,7 @@ public function index() $view->segment = Request::getRawSegmentFromRequest(); $view->ssl = ProxyHttp::isHttps(); + $view->siteUrls = SitesManager\API::getInstance()->getSiteUrlsFromId($this->site->getId()); $this->outputCORSHeaders(); return $view->render(); diff --git a/plugins/Overlay/client/client.js b/plugins/Overlay/client/client.js index 66f20ed1644..0bd74c371e4 100644 --- a/plugins/Overlay/client/client.js +++ b/plugins/Overlay/client/client.js @@ -1,6 +1,6 @@ var Piwik_Overlay_Client = (function () { - var DOMAIN_PARSE_REGEX = /http(s)?:\/\/(www\.)?([^\/]*)/i; + var DOMAIN_PARSE_REGEX = /^http(s)?:\/\/(www\.)?([^\/]*)/i; /** jQuery */ var $; diff --git a/plugins/Overlay/javascripts/Piwik_Overlay.js b/plugins/Overlay/javascripts/Piwik_Overlay.js index 23b761f8fda..f6212ed73e9 100644 --- a/plugins/Overlay/javascripts/Piwik_Overlay.js +++ b/plugins/Overlay/javascripts/Piwik_Overlay.js @@ -7,7 +7,13 @@ var Piwik_Overlay = (function () { - var DOMAIN_PARSE_REGEX = /http(s)?:\/\/(www\.)?([^\/]*)/i; + var DOMAIN_PARSE_REGEX = /^http(s)?:\/\/(www\.)?([^\/]*)/i; + var ORIGIN_PARSE_REGEX = /^https?:\/\/[^\/]*/; + var ALLOWED_API_REQUEST_WHITELIST = [ + 'Overlay.getTranslations', + 'Overlay.getExcludedQueryParameters', + 'Overlay.getFollowingPages', + ]; var $body, $iframe, $sidebar, $main, $location, $loading, $errorNotLoading; var $rowEvolutionLink, $transitionsLink, $visitorLogLink; @@ -29,9 +35,7 @@ var Piwik_Overlay = (function () { $location.html(' ').unbind('mouseenter').unbind('mouseleave'); iframeCurrentPage = currentUrl; - var m = currentUrl.match(DOMAIN_PARSE_REGEX); - iframeDomain = m[3]; - iframeOrigin = m[0]; + iframeDomain = currentUrl.match(DOMAIN_PARSE_REGEX)[3]; var params = { module: 'Overlay', @@ -140,11 +144,30 @@ var Piwik_Overlay = (function () { return location; } + function setIframeOrigin(location) { + iframeOrigin = location.match(ORIGIN_PARSE_REGEX)[0]; + + // unset iframe origin if it is not one of the site URLs + var validSiteOrigins = Piwik_Overlay.siteUrls.map(function (url) { + return url.match(ORIGIN_PARSE_REGEX)[0]; + }); + + if (iframeOrigin && validSiteOrigins.indexOf(iframeOrigin) === -1) { + try { + console.log('Found invalid iframe origin in hash URL: ' + iframeOrigin); + } catch (e) { + // ignore + } + iframeOrigin = null; + } + } + /** $.history callback for hash change */ function hashChangeCallback(urlHash) { var location = getOverlayLocationFromHash(urlHash); location = Overlay_Helper.decodeFrameUrl(location); - iframeOrigin = location.match(DOMAIN_PARSE_REGEX)[0]; + + setIframeOrigin(location); if (location == iframeCurrentPageNormalized) { return; @@ -166,7 +189,7 @@ var Piwik_Overlay = (function () { function handleApiRequests() { window.addEventListener("message", function (event) { - if (event.origin !== iframeOrigin) { + if (event.origin !== iframeOrigin || !iframeOrigin) { return; } @@ -179,6 +202,16 @@ var Piwik_Overlay = (function () { var url = decodeURIComponent(strData[2]); var params = broadcast.getValuesFromUrl(url); + params.module = 'API'; + params.action = 'index'; + + if (ALLOWED_API_REQUEST_WHITELIST.indexOf(params.method) === -1) { + sendResponse({ + result: 'error', + message: "'" + params.method + "' method is not allowed.", + }); + return; + } angular.element(document).injector().invoke(['piwikApi', function (piwikApi) { piwikApi.fetch(params) @@ -328,6 +361,7 @@ var Piwik_Overlay = (function () { window.location.replace(newLocation); } else { // happens when the url is changed by hand or when the l parameter is there on page load + setIframeOrigin(currentUrl); loadSidebar(currentUrl); } } diff --git a/plugins/Overlay/templates/index.twig b/plugins/Overlay/templates/index.twig index 97ed8783ff9..60c64e0081b 100644 --- a/plugins/Overlay/templates/index.twig +++ b/plugins/Overlay/templates/index.twig @@ -65,6 +65,8 @@