diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index d5cab9c63..064958272 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -14,7 +14,7 @@ html, body { .report { font-size: 30px; - max-width: 26em; + max-width: 30em; margin: auto; padding-top: 15px; } diff --git a/www/checkup/checkup-tools.js b/www/checkup/checkup-tools.js index f9259ff28..5e0eef42d 100644 --- a/www/checkup/checkup-tools.js +++ b/www/checkup/checkup-tools.js @@ -1,5 +1,7 @@ define([ -], function () { + 'jquery', + '/common/common-util.js', +], function ($, Util) { var Tools = {}; Tools.supportsSharedArrayBuffers = function () { try { @@ -51,5 +53,25 @@ define([ return navigator.userAgent + "\n" + navigator.vendor; }; + Tools.cacheBuster = function (url) { + if (/\?/.test(url)) { return url; } + return url + '?test=' + (+new Date()); + }; + + var common_map = {}; + Tools.common_xhr = function (url, _cb) { + var cb = Util.once(Util.once(Util.mkAsync(_cb))); + var ready = common_map[url]; + if (ready) { return void ready.reg(cb); } + ready = common_map[url] = Util.mkEvent(true); + ready.reg(cb); + return void $.ajax(Tools.cacheBuster(url), { + dataType: 'text', + complete: function (xhr) { + ready.fire(xhr); + }, + }); + }; + return Tools; }); diff --git a/www/checkup/main.js b/www/checkup/main.js index 8d8452726..520320372 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -67,9 +67,7 @@ define([ $(msg).removeClass('cp-danger').addClass('cp-warning'); }; - var cacheBuster = function (url) { - return url + '?test=' + (+new Date()); - }; + var cacheBuster = Tools.cacheBuster; var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); @@ -834,7 +832,7 @@ define([ }); }); - assert(function (cb, msg) { + assert(function (cb, msg) { // XXX this test has been superceded // check that the sandbox domain is included in connect-src msg.appendChild(h('span', [ "This instance's ", @@ -848,20 +846,17 @@ define([ " See the provided NGINX configuration file for an example of how to set this header correctly.", ])); - $.ajax(cacheBuster('/'), { - dataType: 'text', - complete: function (xhr) { - var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); - var connect = (CSP && CSP['connect-src']) || ""; - if (connect.includes(trimmedSafe)) { - return void cb(true); - } - cb(CSP); - }, + Tools.common_xhr('/', function (xhr) { + var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); + var connect = (CSP && CSP['connect-src']) || ""; + if (connect.includes(trimmedSafe)) { + return void cb(true); + } + cb(CSP); }); }); - assert(function (cb, msg) { + assert(function (cb, msg) { // XXX this test has been superceded var directives = [ 'img-src', 'media-src', @@ -882,24 +877,26 @@ define([ code('frame-src'), " in the provided NGINX configuration file for an example of how to set these headers correctly.", ])); - $.ajax(cacheBuster('/'), { - dataType: 'text', - complete: function (xhr) { - var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); - // check that the relevant CSP directives are defined - // and that none of them permit general remote content via '*' - if (directives.every(function (k) { - return typeof(CSP[k]) === 'string' && !/ \* /.test(CSP[k]); - })) { - return void cb(true); - } - cb(CSP); - }, + Tools.common_xhr('/', function (xhr) { + var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); + // check that the relevant CSP directives are defined + // and that none of them permit general remote content via '*' + if (directives.every(function (k) { + return typeof(CSP[k]) === 'string' && !/ \* /.test(CSP[k]); + })) { + return void cb(true); + } + cb(CSP); }); }); assert(function (cb, msg) { - msg.appendChild(h('span', 'pewpew')); + msg.appendChild(h('span', [ // XXX + code('/api/config'), + " returned an HTTP status code other than ", + code('200'), + ' when accessed from the sandbox domain.', + ])); deferredPostMessage({ command: 'CHECK_HTTP_STATUS', content: { @@ -910,6 +907,195 @@ define([ }); }); +/* + assert(function (cb, msg) { + msg.appendChild(h('span', [ + 'all headers', + ])); + + Tools.common_xhr('/', function (xhr) { + var all_headers = xhr.getAllResponseHeaders().split(/\r|\n/).filter(Boolean); + cb(all_headers); + }); + }); +*/ + + var validateCSP = function (raw, expected) { + var CSP = parseCSP(raw); + var checkRule = function (attr, rules) { + var h = CSP[attr]; + // return `true` if you fail this test... + if (typeof(h) !== 'string' || !h) { return true; } + var l = rules.length; + for (var i = 0;i < l;i++) { + if (!h.includes(rules[i])) { + console.log("BAD_HEADER", rules[i]); + return true; + } + h = h.replace(rules[i], ''); + } + return h.trim(); + }; + if (Object.keys(expected).some(function (dir) { + var result = checkRule(dir, expected[dir]); + if (result) { + console.log('BAD_HEADER:', { + rule: dir, + expected: expected[dir], + result: result, + }); + } + + return result; + })) { + return parseCSP(raw); + } + return true; + }; + + assert(function (_cb, msg) { + var url = '/sheet/inner.html'; + var cb = Util.once(Util.mkAsync(_cb)); + msg.appendChild(CSP_WARNING(url)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (raw) { + var $outer = trimmedUnsafe; + var $sandbox = trimmedSafe; + var result = validateCSP(raw, { + 'default-src': ["'none'"], + 'style-src': ["'unsafe-inline'", "'self'", $outer], + 'font-src': ["'self'", 'data:', $outer], + 'child-src': ["'self'", 'blob:', $outer, $sandbox], + 'frame-src': ["'self'", 'blob:', $outer, $sandbox], + 'script-src': ["'self'", 'resource:', $outer, + "'unsafe-eval'", // XXX sloppy onlyoffice BS + "'unsafe-inline'", // XXX sloppy onlyoffice BS + ], + 'connect-src': [ + "'self'", + 'blob:', + $outer, + $sandbox, + /https:/.test($outer)? '': 'ws:', // XXX warn about ws: unless the origin is unencrypted + 'wss:', // XXX always accept wss: ??? + ], + + 'img-src': ["'self'", 'data:', 'blob:', $outer], + 'media-src': ['blob:'], + 'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding + 'worker-src': ["'self'", $outer, $sandbox], + }); + cb(result); + }); + }); + + assert(function (cb, msg) { + var header = 'content-security-policy'; + msg.appendChild(h('span', [ + header, + ])); + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + var $outer = trimmedUnsafe; + var $sandbox = trimmedSafe; + var result = validateCSP(raw, { + 'default-src': ["'none'"], + 'style-src': ["'unsafe-inline'", "'self'", $outer], + 'font-src': ["'self'", 'data:', $outer], + 'child-src': ["'self'", 'blob:', $outer, $sandbox], + 'frame-src': ["'self'", 'blob:', $outer, $sandbox], + 'script-src': ["'self'", 'resource:', $outer], + 'connect-src': [ + "'self'", + 'blob:', + $outer, + $sandbox, + /https:/.test($outer)? '': 'ws:', // XXX warn about ws: unless the origin is unencrypted + 'wss:', // XXX always accept wss: ??? + ], + 'img-src': ["'self'", 'data:', 'blob:', $outer], + 'media-src': ['blob:'], + 'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding + 'worker-src': ["'self'", $outer, $sandbox], + }); + + cb(result); + }); + }); + + assert(function (cb, msg) { + var header = 'access-control-allow-origin'; + msg.appendChild(h('span', [ + header, + ])); + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(raw === "*" || raw); // XXX + }); + }); + + assert(function (cb, msg) { + var header = 'cross-origin-embedder-policy'; + msg.appendChild(h('span', [ + header, + ])); + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(raw === 'require-corp' || raw); // XXX + }); + }); + + assert(function (cb, msg) { + var header = 'cross-origin-resource-policy'; + msg.appendChild(h('span', [ + header, + ])); + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(raw === 'cross-origin' || raw); // XXX + }); + }); + + assert(function (cb, msg) { + var header = 'X-Content-Type-Options'; + msg.appendChild(h('span', [ + header, + ])); + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(raw === 'nosniff' || raw); // XXX + }); + }); + + assert(function (cb, msg) { + var header = 'Cache-Control'; + msg.appendChild(h('span', [ + header, + ])); + // Cache-Control should be 'no-cache' unless the URL includes ver= + Tools.common_xhr('/', function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(raw === 'no-cache' || raw); // XXX + }); + }); + + assert(function (cb, msg) { + var header = 'Cache-Control'; + msg.appendChild(h('span', [ + header, + ])); + // Cache-Control should be 'max-age=' if the URL includes 'ver=' + Tools.common_xhr('/?ver=' +(+new Date()), function (xhr) { + var raw = xhr.getResponseHeader(header); + cb(/max\-age=\d+$/.test(raw) || raw); // XXX + }); + }); + /* assert(function (cb, msg) { setWarningClass(msg); diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js index 47a7be13b..22130b3cf 100644 --- a/www/checkup/sandbox/main.js +++ b/www/checkup/sandbox/main.js @@ -11,13 +11,10 @@ define([ window.parent.postMessage(JSON.stringify(content), '*'); }; postMessage({ command: "READY", }); - var getHeaders = function (url, cb) { - $.ajax(url + "?test=" + (+new Date()), { - dataType: 'text', - complete: function (xhr) { - var allHeaders = xhr.getAllResponseHeaders(); - return void cb(void 0, allHeaders, xhr); - }, + var getHeaders = function (url, cb) { // XXX reuse XHR objects? + Tools.common_xhr(url, function (xhr) { + var allHeaders = xhr.getAllResponseHeaders(); + return void cb(void 0, allHeaders, xhr); }); }; var COMMANDS = {}; @@ -49,11 +46,8 @@ define([ }; COMMANDS.CHECK_HTTP_STATUS = function (content, cb) { - $.ajax(content.url, { - dataType: 'text', - complete: function (xhr) { - cb(xhr.status); - }, + Tools.common_xhr(content.url, function (xhr) { + cb(xhr.status); }); };