add new, very specific tests for CSP to the checkup page

pull/1/head
ansuz 3 years ago
parent 5bf21a25c0
commit 383684d339

@ -16,15 +16,15 @@ Default.commonCSP = function (domain, sandbox) {
* it is recommended that you configure these fields to match the * it is recommended that you configure these fields to match the
* domain which will serve your CryptPad instance. * domain which will serve your CryptPad instance.
*/ */
"child-src 'self' blob: " + domain + sandbox, "child-src " + domain,
// IE/Edge // IE/Edge
"frame-src 'self' blob: " + domain + sandbox, "frame-src 'self' blob: " + sandbox,
/* this allows connections over secure or insecure websockets /* this allows connections over secure or insecure websockets
if you are deploying to production, you'll probably want to remove if you are deploying to production, you'll probably want to remove
the ws://* directive, and change '*' to your domain the ws://* directive
*/ */
"connect-src 'self' ws: wss: blob: " + domain + sandbox, "connect-src 'self' ws: blob: " + domain + sandbox,
// data: is used by codemirror // data: is used by codemirror
"img-src 'self' data: blob:" + domain, "img-src 'self' data: blob:" + domain,
@ -32,6 +32,7 @@ Default.commonCSP = function (domain, sandbox) {
// for accounts.cryptpad.fr authentication and cross-domain iframe sandbox // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox
"frame-ancestors *", "frame-ancestors *",
"worker-src 'self'",
"" ""
]; ];
}; };

@ -84,12 +84,6 @@ var setHeaders = (function () {
} }
if (Object.keys(headers).length) { if (Object.keys(headers).length) {
return function (req, res) { return function (req, res) {
// apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere
/*
applyHeaderMap(res, {
"Cross-Origin-Opener-Policy": /^\/(sheet|presentation|doc|convert)\//.test(req.url)? 'same-origin': '',
});*/
if (Env.NO_SANDBOX) { // handles correct configuration for local development if (Env.NO_SANDBOX) { // handles correct configuration for local development
// https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs
applyHeaderMap(res, { applyHeaderMap(res, {

@ -14,7 +14,7 @@ html, body {
.report { .report {
font-size: 30px; font-size: 30px;
max-width: 26em; max-width: 30em;
margin: auto; margin: auto;
padding-top: 15px; padding-top: 15px;
} }

@ -1,5 +1,7 @@
define([ define([
], function () { 'jquery',
'/common/common-util.js',
], function ($, Util) {
var Tools = {}; var Tools = {};
Tools.supportsSharedArrayBuffers = function () { Tools.supportsSharedArrayBuffers = function () {
try { try {
@ -51,5 +53,25 @@ define([
return navigator.userAgent + "\n" + navigator.vendor; 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; return Tools;
}); });

@ -67,9 +67,7 @@ define([
$(msg).removeClass('cp-danger').addClass('cp-warning'); $(msg).removeClass('cp-danger').addClass('cp-warning');
}; };
var cacheBuster = function (url) { var cacheBuster = Tools.cacheBuster;
return url + '?test=' + (+new Date());
};
var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin);
var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin);
@ -371,7 +369,6 @@ define([
var expect = { var expect = {
'cross-origin-resource-policy': 'cross-origin', 'cross-origin-resource-policy': 'cross-origin',
'cross-origin-embedder-policy': 'require-corp', '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, { $.ajax(url, {
@ -656,7 +653,7 @@ define([
]); ]);
}; };
assert(function (_cb, msg) { assert(function (_cb, msg) { // FIXME possibly superseded by more advanced CSP tests?
var url = '/sheet/inner.html'; var url = '/sheet/inner.html';
var cb = Util.once(Util.mkAsync(_cb)); var cb = Util.once(Util.mkAsync(_cb));
msg.appendChild(CSP_WARNING(url)); msg.appendChild(CSP_WARNING(url));
@ -672,7 +669,7 @@ define([
}); });
}); });
assert(function (cb, msg) { assert(function (cb, msg) { // FIXME possibly superseded by more advanced CSP tests?
var url = '/common/onlyoffice/v5/web-apps/apps/spreadsheeteditor/main/index.html'; var url = '/common/onlyoffice/v5/web-apps/apps/spreadsheeteditor/main/index.html';
msg.appendChild(CSP_WARNING(url)); msg.appendChild(CSP_WARNING(url));
deferredPostMessage({ deferredPostMessage({
@ -805,36 +802,7 @@ define([
cb(isHTTPS(trimmedUnsafe) && isHTTPS(trimmedSafe)); cb(isHTTPS(trimmedUnsafe) && isHTTPS(trimmedSafe));
}); });
[ assert(function (cb, msg) { // FIXME this test has been superceded, but the descriptive text is still useful
//'sheet',
//'presentation',
//'doc',
//'convert',
].forEach(function (url) {
assert(function (cb, msg) {
var header = 'cross-origin-opener-policy';
var expected = 'same-origin';
deferredPostMessage({
command: 'GET_HEADER',
content: {
url: '/' + url + '/',
header: header,
}
}, function (content) {
msg.appendChild(h('span', [
code(url),
' was served without the correct ',
code(header),
' HTTP header value (',
code(expected),
'). This will interfere with your ability to convert between office file formats.'
]));
cb(content === expected);
});
});
});
assert(function (cb, msg) {
// check that the sandbox domain is included in connect-src // check that the sandbox domain is included in connect-src
msg.appendChild(h('span', [ msg.appendChild(h('span', [
"This instance's ", "This instance's ",
@ -848,65 +816,254 @@ define([
" See the provided NGINX configuration file for an example of how to set this header correctly.", " See the provided NGINX configuration file for an example of how to set this header correctly.",
])); ]));
$.ajax(cacheBuster('/'), { Tools.common_xhr('/', function (xhr) {
dataType: 'text', var CSP = parseCSP(xhr.getResponseHeader('content-security-policy'));
complete: function (xhr) { var connect = (CSP && CSP['connect-src']) || "";
var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); if (connect.includes(trimmedSafe)) {
var connect = (CSP && CSP['connect-src']) || ""; return void cb(true);
if (connect.includes(trimmedSafe)) { }
return void cb(true); cb(CSP);
}
cb(CSP);
},
}); });
}); });
assert(function (cb, msg) { assert(function (cb, msg) {
var directives = [
'img-src',
'media-src',
'child-src',
'frame-src'
];
msg.appendChild(h('span', [ msg.appendChild(h('span', [
"This instance's ", code('/api/config'),
code("Content-Security-Policy"), " returned an HTTP status code other than ",
" headers are unnecessarily permissive.", code('200'),
h('br'), ' when accessed from the sandbox domain.',
h('br'),
" Review the recommended settings for ",
code('img-src'), ', ',
code('media-src'), ', ',
code('child-src'), ', and ',
code('frame-src'),
" in the provided NGINX configuration file for an example of how to set these headers correctly.",
])); ]));
$.ajax(cacheBuster('/'), { deferredPostMessage({
dataType: 'text', command: 'CHECK_HTTP_STATUS',
complete: function (xhr) { content: {
var CSP = parseCSP(xhr.getResponseHeader('content-security-policy')); url: cacheBuster('/api/config'),
// 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);
}, },
}, function (content) {
cb(content === 200 || content);
}); });
}); });
/*
assert(function (cb, msg) { assert(function (cb, msg) {
msg.appendChild(h('span', 'pewpew')); 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, msg, expected) {
var CSP = parseCSP(raw);
var checkRule = function (attr, rules) {
var v = CSP[attr];
// return `true` if you fail this test...
if (typeof(v) !== 'string' || !v) { return true; }
var l = rules.length;
for (var i = 0;i < l;i++) {
if (typeof(rules[i]) !== 'undefined' && !v.includes(rules[i])) { return true; }
v = v.replace(rules[i], '');
}
return v.trim();
};
if (Object.keys(expected).some(function (dir) {
var result = checkRule(dir, expected[dir]);
if (result) {
msg.appendChild(h('p', [
'A value of ',
code('"' + expected[dir].filter(Boolean).join(' ') + '"'),
' was expected for the ',
code(dir),
' directive.',
]));
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(h('span', [
code(trimmedUnsafe + url),
' was served with incorrect ',
code('Content-Security-Policy'),
' headers.',
]));
//msg.appendChild(CSP_WARNING(url));
deferredPostMessage({ deferredPostMessage({
command: 'CHECK_HTTP_STATUS', command: 'GET_HEADER',
content: { content: {
url: cacheBuster('/api/config'), url: url,
header: 'content-security-policy',
}, },
}, function (content) { }, function (raw) {
cb(content === 200 || content); var $outer = trimmedUnsafe;
var $sandbox = trimmedSafe;
var result = validateCSP(raw, msg, {
'default-src': ["'none'"],
'style-src': ["'unsafe-inline'", "'self'", $outer],
'font-src': ["'self'", 'data:', $outer],
'child-src': [$outer], //["'self'", 'blob:', $outer, $sandbox],
'frame-src': ["'self'", 'blob:', /*$outer, */$sandbox],
'script-src': ["'self'", 'resource:', $outer,
"'unsafe-eval'",
"'unsafe-inline'",
],
'connect-src': [
"'self'",
'blob:',
$outer,
$sandbox,
/https:\/\//.test($outer)? $outer.replace('https://', 'wss://') : 'ws:',
],
'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', [
code(trimmedUnsafe + '/'),
' was served with incorrect ',
code('Content-Security-Policy'),
' headers.',
]));
Tools.common_xhr('/', function (xhr) {
var raw = xhr.getResponseHeader(header);
var $outer = trimmedUnsafe;
var $sandbox = trimmedSafe;
var result = validateCSP(raw, msg, {
'default-src': ["'none'"],
'style-src': ["'unsafe-inline'", "'self'", $outer],
'font-src': ["'self'", 'data:', $outer],
'child-src': [$outer], //["'self'", 'blob:', $outer, $sandbox],
'frame-src': ["'self'", 'blob:', /*$outer,*/ $sandbox],
'script-src': ["'self'", 'resource:', $outer],
'connect-src': [
"'self'",
'blob:',
$outer,
$sandbox,
/https:\/\//.test($outer)? $outer.replace('https://', 'wss://') : 'ws:',
],
'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', [
'Assets must be served with an ',
code(header),
' header with a value of ',
code("'*'"),
' if you wish to support embedding of encrypted media on third party websites.',
]));
Tools.common_xhr('/', function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(raw === "*" || raw);
});
});
assert(function (cb, msg) {
var header = 'Cross-Origin-Embedder-Policy';
msg.appendChild(h('span', [
"Assets must be served with a ",
code(header),
' value of ',
code('require-corp'),
" to enable browser features required for client-side document conversion.",
]));
Tools.common_xhr('/', function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(raw === 'require-corp' || raw);
});
});
assert(function (cb, msg) {
var header = 'Cross-Origin-Resource-Policy';
msg.appendChild(h('span', [
"Assets must be served with a ",
code(header),
' value of ',
code('cross-origin'),
" to enable browser features required for client-side document conversion.",
]));
Tools.common_xhr('/', function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(raw === 'cross-origin' || raw);
});
});
assert(function (cb, msg) {
var header = 'X-Content-Type-Options';
msg.appendChild(h('span', [
"Assets should be served with an ",
code(header),
' header with a value of ',
code('nosniff'),
'.',
]));
Tools.common_xhr('/', function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(raw === 'nosniff' || raw);
});
});
assert(function (cb, msg) {
var header = 'Cache-Control';
msg.appendChild(h('span', [
'Assets requested without a version parameter should be served with a ',
code('no-cache'),
' value for the ',
code("Cache-Control"),
' 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);
});
});
assert(function (cb, msg) {
var header = 'Cache-Control';
msg.appendChild(h('span', [
'Assets requested with a version parameter should be served with a long-lived ',
code('Cache-Control'),
' header.',
]));
// Cache-Control should be 'max-age=<number>' if the URL includes 'ver='
Tools.common_xhr('/customize/messages.js?ver=' +(+new Date()), function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(/max\-age=\d+$/.test(raw) || raw);
}); });
}); });
@ -959,13 +1116,6 @@ define([
}); });
*/ */
if (false) {
assert(function (cb, msg) {
msg.innerText = 'fake test to simulate failure';
cb(false);
});
}
var row = function (cells) { var row = function (cells) {
return h('tr', cells.map(function (cell) { return h('tr', cells.map(function (cell) {
return h('td', cell); return h('td', cell);

@ -11,13 +11,10 @@ define([
window.parent.postMessage(JSON.stringify(content), '*'); window.parent.postMessage(JSON.stringify(content), '*');
}; };
postMessage({ command: "READY", }); postMessage({ command: "READY", });
var getHeaders = function (url, cb) { var getHeaders = function (url, cb) { // XXX reuse XHR objects?
$.ajax(url + "?test=" + (+new Date()), { Tools.common_xhr(url, function (xhr) {
dataType: 'text', var allHeaders = xhr.getAllResponseHeaders();
complete: function (xhr) { return void cb(void 0, allHeaders, xhr);
var allHeaders = xhr.getAllResponseHeaders();
return void cb(void 0, allHeaders, xhr);
},
}); });
}; };
var COMMANDS = {}; var COMMANDS = {};
@ -49,11 +46,8 @@ define([
}; };
COMMANDS.CHECK_HTTP_STATUS = function (content, cb) { COMMANDS.CHECK_HTTP_STATUS = function (content, cb) {
$.ajax(content.url, { Tools.common_xhr(content.url, function (xhr) {
dataType: 'text', cb(xhr.status);
complete: function (xhr) {
cb(xhr.status);
},
}); });
}; };

Loading…
Cancel
Save