From cc5674585899e24339aa7b4d7899269d483cbe7c Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 20:35:48 +0530 Subject: [PATCH] add more thorough tests for sandbox configuration on the checkup page --- www/checkup/app-checkup.less | 6 -- www/checkup/main.js | 194 +++++++++++++++++++++++++++++++---- www/checkup/sandbox/main.js | 183 ++++++--------------------------- 3 files changed, 202 insertions(+), 181 deletions(-) diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index 99999e2bb..a475d5983 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -12,12 +12,6 @@ html, body { color: @cryptpad_text_col; font-family: "IBM Plex Mono"; - iframe.sandbox-test { - display: block; - width: 100%; - height: 100%; - } - .report { font-size: 30px; max-width: 50%; diff --git a/www/checkup/main.js b/www/checkup/main.js index 896156fc6..3ac3b190a 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -20,7 +20,6 @@ define([ ], function ($, ApiConfig, Assertions, h, Messages, DomReady, nThen, SFCommonO, Login, Hash, Util, Pinpad, NetConfig, Pages) { - var Assert = Assertions(); var trimSlashes = function (s) { if (typeof(s) !== 'string') { return s; } @@ -51,15 +50,12 @@ define([ ]); }; + var cacheBuster = function (url) { + return url + '?test=' + (+new Date()); + }; + var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); -/* -// XXX display results from this iframe on this page - document.body.appendChild(h('iframe', { - class: 'sandbox-test', - src: trimmedSafe + '/checkup/sandbox/index.html', - })); -*/ assert(function (cb, msg) { msg.appendChild(h('span', [ @@ -125,7 +121,7 @@ define([ var checkAvailability = function (url, cb) { $.ajax({ - url: url, // XXX bust cache + url: cacheBuster(url), data: {}, complete: function (xhr) { cb(xhr.status === 200); @@ -166,7 +162,6 @@ define([ ])); var to; - var obj; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { @@ -174,13 +169,8 @@ define([ console.error('TIMEOUT loading iframe on the safe domain'); cb(false); }, 5000); - obj = SFCommonO.initIframe(waitFor); + SFCommonO.initIframe(waitFor); }).nThen(function () { - SFCommonO.start({ - href: obj.href, - }); - }).nThen(function () { - console.error("DONE?"); // Iframe is loaded clearTimeout(to); cb(true); @@ -351,14 +341,14 @@ define([ assert(function (cb, msg) { msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; - var url = sheetURL; + var url = cacheBuster(sheetURL); var expect = { 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', //'cross-origin-opener-policy': 'same-origin', // FIXME this is in our nginx config but not server.js }; - $.ajax(url, { // XXX bust cache + $.ajax(url, { complete: function (xhr) { cb(!Object.keys(expect).some(function (k) { var response = xhr.getResponseHeader(k); @@ -430,7 +420,7 @@ define([ "Your browser console may provide more details as to why this resource could not be loaded. ", ])); - $.ajax('/api/broadcast', { // XXX bust cache + $.ajax(cacheBuster('/api/broadcast'), { dataType: 'text', complete: function (xhr) { cb(xhr.status === 200); @@ -443,7 +433,7 @@ define([ }; var checkAPIHeaders = function (url, msg, cb) { - $.ajax(url, { // XXX bust cache + $.ajax(cacheBuster(url), { dataType: 'text', complete: function (xhr) { var allHeaders = xhr.getAllResponseHeaders(); @@ -495,13 +485,13 @@ define([ var INCORRECT_HEADER_TEXT = ' was served with duplicated or incorrect headers. Compare your reverse-proxy configuration against the provided example.'; assert(function (cb, msg) { - var url = '/api/config'; // XXX bust cache + var url = '/api/config'; msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { - var url = '/api/broadcast'; // XXX bust cache + var url = '/api/broadcast'; msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); @@ -559,6 +549,166 @@ define([ cb(false); }); + var response = Util.response(function (err) { + console.error('SANDBOX_ERROR', err); + }); + + var sandboxIframe = h('iframe', { + class: 'sandbox-test', + src: cacheBuster(trimmedSafe + '/checkup/sandbox/index.html'), + }); + document.body.appendChild(sandboxIframe); + + var sandboxIframeReady = Util.mkEvent(true); + setTimeout(function () { + sandboxIframeReady.fire("TIMEOUT"); + }, 10 * 1000); + + var postMessage = function (content, cb) { + try { + var txid = Util.uid(); + content.txid = txid; + response.expect(txid, cb, 15000); + sandboxIframe.contentWindow.postMessage(JSON.stringify(content), '*'); + } catch (err) { + console.error(err); + } + }; + + window.addEventListener('message', function (event) { + try { + var msg = JSON.parse(event.data); + if (msg.command === 'READY') { return void sandboxIframeReady.fire(); } + var txid = msg.txid; + if (!txid) { return console.log("no handler for ", txid); } + response.handle(txid, msg.content); + } catch (err) { + console.error(event); + console.error(err); + } + }); + + var parseCSP = function (CSP) { + //console.error(CSP); + var CSP_headers = {}; + CSP.split(";") + .forEach(function (rule) { + rule = (rule || "").trim(); + if (!rule) { return; } + var parts = rule.split(/\s/); + var first = parts[0]; + var rest = rule.slice(first.length + 1); + CSP_headers[first] = rest; + //console.error(rule.trim()); + //console.info("[%s] '%s'", first, rest); + }); + return CSP_headers; + }; + + var hasUnsafeEval = function (CSP_headers) { + return /unsafe\-eval/.test(CSP_headers['script-src']); + }; + + var hasUnsafeInline = function (CSP_headers) { + return /unsafe\-inline/.test(CSP_headers['script-src']); + }; + + var hasOnlyOfficeHeaders = function (CSP_headers) { + if (!hasUnsafeEval(CSP_headers)) { + console.error("NO_UNSAFE_EVAL"); + console.log(CSP_headers); + return false; + } + if (!hasUnsafeInline(CSP_headers)) { + console.error("NO_UNSAFE_INLINE"); + return void false; + } + return true; + }; + + var CSP_WARNING = function (url) { + return h('span', [ + code(url), + ' does not have the required ', + code("'content-security-policy'"), + ' headers set. This is most often related to incorrectly configured sandbox domains or reverse proxies.', + ]); + }; + + assert(function (_cb, msg) { + var url = '/sheet/inner.html'; + var cb = Util.once(Util.mkAsync(_cb)); + msg.appendChild(CSP_WARNING(url)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + }); + + assert(function (cb, msg) { + var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + msg.appendChild(CSP_WARNING(url)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + }); + + assert(function (cb, msg) { + var url = '/sheet/inner.html'; + msg.appendChild(h('span', [ + code(url), + ' does not have the required ', + code("'cross-origin-opener-policy'"), + ' headers set.', + ])); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'cross-origin-opener-policy', + }, + }, function (content) { + cb(content === 'same-origin'); + }); + }); + }); + if (false) { assert(function (cb, msg) { msg.innerText = 'fake test to simulate failure'; diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js index 8066dc1b3..7450ed0e3 100644 --- a/www/checkup/sandbox/main.js +++ b/www/checkup/sandbox/main.js @@ -1,34 +1,16 @@ define([ 'jquery', - '/api/config', - '/assert/assertions.js', - '/common/hyperscript.js', - '/customize/messages.js', - '/common/dom-ready.js', - '/bower_components/nthen/index.js', - '/common/sframe-common-outer.js', - '/customize/login.js', - '/common/common-hash.js', - '/common/common-util.js', - '/common/pinpad.js', - '/common/outer/network-config.js', - '/customize/pages.js', + //'/bower_components/nthen/index.js', + //'/common/common-util.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/checkup/app-checkup.less', -], function ($, ApiConfig, Assertions, h, Messages, DomReady, - nThen, SFCommonO, Login, Hash, Util, Pinpad, - NetConfig, Pages) { - var Assert = Assertions(); - var assert = function (f, msg) { - Assert(f, msg || h('span.advisory-text.cp-danger')); +], function ($ /*, nThen, Util */) { + var postMessage = function (content) { + window.parent.postMessage(JSON.stringify(content), '*'); }; - - var code = function (content) { - return h('code', content); - }; - + postMessage({ command: "READY", }); var getHeaders = function (url, cb) { $.ajax(url + "?test=" + (+new Date()), { dataType: 'text', @@ -38,138 +20,33 @@ define([ }, }); }; - var parseCSP = function (CSP) { - //console.error(CSP); - var CSP_headers = {}; - CSP.split(";") - .forEach(function (rule) { - rule = (rule || "").trim(); - if (!rule) { return; } - var parts = rule.split(/\s/); - var first = parts[0]; - var rest = rule.slice(first.length + 1); - CSP_headers[first] = rest; - //console.error(rule.trim()); - console.info("[%s] '%s'", first, rest); - }); - return CSP_headers; - }; - - var hasUnsafeEval = function (CSP_headers) { - return /unsafe\-eval/.test(CSP_headers['script-src']); - }; - - var hasUnsafeInline = function (CSP_headers) { - return /unsafe\-inline/.test(CSP_headers['script-src']); - }; - - var hasOnlyOfficeHeaders = function (CSP_headers) { - if (!hasUnsafeEval(CSP_headers)) { - console.error("NO_UNSAFE_EVAL"); - console.log(CSP_headers); - return false; - } - if (!hasUnsafeInline(CSP_headers)) { - console.error("NO_UNSAFE_INLINE"); - return void false; - } - return true; - }; - - // XXX run these from /checkup/inner.js and report to /checkup/main.js - assert(function (cb, msg) { - var url = '/sheet/inner.html'; - msg.appendChild(h('span', [ - code(url), - ' has the wrong headers.', - ])); + var COMMANDS = {}; + COMMANDS.GET_HEADER = function (content, cb) { + var url = content.url; getHeaders(url, function (err, headers, xhr) { - var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); - cb(hasOnlyOfficeHeaders(CSP_headers)); + cb(xhr.getResponseHeader(content.header)); }); - }); - - assert(function (cb, msg) { - var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; - msg.appendChild(h('span', [ - code(url), - ' has the wrong headers.', - ])); - getHeaders(url, function (err, headers, xhr) { - var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); - cb(hasOnlyOfficeHeaders(CSP_headers)); - }); - }); - - var row = function (cells) { - return h('tr', cells.map(function (cell) { - return h('td', cell); - })); }; - var failureReport = function (obj) { - return h('div.error', [ - h('h5', obj.message), - h('table', [ - row(["Failed test number", obj.test + 1]), - row(["Returned value", obj.output]), - ]), - ]); - }; - - var completed = 0; - var $progress = $('#cp-progress'); - - var versionStatement = function () { - return h('p', [ - "This instance is running ", - h('span.cp-app-checkup-version',[ - "CryptPad", - ' ', - Pages.versionString, - ]), - '.', - ]); - }; - - Assert.run(function (state) { - var errors = state.errors; - var failed = errors.length; - - Messages.assert_numberOfTestsPassed = "{0} / {1} tests passed."; - - var statusClass = failed? 'failure': 'success'; - - var failedDetails = "Details found below"; - var successDetails = "This checkup only tests the most common configuration issues. You may still experience errors or incorrect behaviour."; - var details = h('p', failed? failedDetails: successDetails); - - var summary = h('div.summary.' + statusClass, [ - versionStatement(), - h('p', Messages._getKey('assert_numberOfTestsPassed', [ - state.passed, - state.total - ])), - details, - ]); - - var report = h('div.report', [ - summary, - h('div.failures', errors.map(failureReport)), - ]); - - $progress.remove(); - $('body').prepend(report); - }, function (i, total) { - console.log('test '+ i +' completed'); - completed++; - Messages.assert_numberOfTestsCompleted = "{0} / {1} tests completed."; - $progress.html('').append(h('div.report.pending.summary', [ - versionStatement(), - h('p', [ - h('i.fa.fa-spinner.fa-pulse'), - h('span', Messages._getKey('assert_numberOfTestsCompleted', [completed, total])) - ]) - ])); + window.addEventListener("message", function (event) { + if (event && event.data) { + try { + //console.log(JSON.parse(event.data)); + var msg = JSON.parse(event.data); + var command = msg.command; + var txid = msg.txid; + COMMANDS[command](msg.content, function (response) { + // postMessage with same txid + postMessage({ + txid: txid, + content: response, + }); + }); + } catch (err) { + console.error(err); + } + } else { + console.error(event); + } }); });