adjust CSP headers for printing from OnlyOffice

* allow outer to load resources from the sandbox (for fonts)
* test whether the expected CSP values are present on the checkup page
* simplify the nodejs server a bit
pull/1/head
ansuz 3 years ago
parent 3fd30a30f7
commit b8d6af7891

@ -1,6 +1,6 @@
var Default = module.exports; var Default = module.exports;
Default.commonCSP = function (domain) { Default.commonCSP = function (domain, sandbox) {
domain = ' ' + domain; domain = ' ' + domain;
// Content-Security-Policy // Content-Security-Policy
@ -23,7 +23,7 @@ Default.commonCSP = function (domain) {
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, and change '*' to your domain
*/ */
"connect-src 'self' ws: wss: blob:" + domain, "connect-src 'self' ws: wss: blob: " + domain + (sandbox && sandbox !== domain? ' ' + sandbox: ''),
// data: is used by codemirror // data: is used by codemirror
"img-src 'self' data: blob:" + domain, "img-src 'self' data: blob:" + domain,
@ -35,12 +35,12 @@ Default.commonCSP = function (domain) {
]; ];
}; };
Default.contentSecurity = function (domain) { Default.contentSecurity = function (domain, sandbox) {
return (Default.commonCSP(domain).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' '); return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' ');
}; };
Default.padContentSecurity = function (domain) { Default.padContentSecurity = function (domain, sandbox) {
return (Default.commonCSP(domain).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' '); return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' ');
}; };
Default.httpHeaders = function () { Default.httpHeaders = function () {

@ -17,6 +17,10 @@ var canonicalizeOrigin = function (s) {
return (s || '').trim().replace(/\/+$/, ''); return (s || '').trim().replace(/\/+$/, '');
}; };
var isValidPort = function (p) {
return typeof(p) === 'number' && p < 65535;
};
module.exports.create = function (config) { module.exports.create = function (config) {
const Env = { const Env = {
version: Package.version, version: Package.version,
@ -25,6 +29,9 @@ module.exports.create = function (config) {
httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin), httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin),
httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin), httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin),
removeDonateButton: config.removeDonateButton, removeDonateButton: config.removeDonateButton,
httpPort: isValidPort(config.httpPort)? config.httpPort: 3000,
httpAddress: typeof(config.httpAddress) === 'string'? config.httpAddress: '127.0.0.1',
websocketPath: config.externalWebsocketURL,
OFFLINE_MODE: false, OFFLINE_MODE: false,
FRESH_KEY: '', FRESH_KEY: '',

@ -23,28 +23,24 @@ var fancyURL = function (domain, path) {
return false; return false;
}; };
var deriveSandboxOrigin = function (unsafe, port) {
var url = new URL(unsafe);
url.port = port;
return url.origin;
};
(function () { (function () {
// you absolutely must provide an 'httpUnsafeOrigin' (a truthy string) // you absolutely must provide an 'httpUnsafeOrigin' (a truthy string)
if (!Env.httpUnsafeOrigin || typeof(Env.httpUnsafeOrigin) !== 'string') { if (!Env.httpUnsafeOrigin || typeof(Env.httpUnsafeOrigin) !== 'string') {
throw new Error("No 'httpUnsafeOrigin' provided"); throw new Error("No 'httpUnsafeOrigin' provided");
} }
// fall back to listening on a local address
// if httpAddress is not a string
if (typeof(config.httpAddress) !== 'string') {
config.httpAddress = '127.0.0.1';
}
// listen on port 3000 if a valid port number was not provided
if (typeof(config.httpPort) !== 'number' || config.httpPort > 65535) {
config.httpPort = 3000;
}
if (typeof(Env.httpSafeOrigin) !== 'string') { if (typeof(Env.httpSafeOrigin) !== 'string') {
Env.NO_SANDBOX = true; Env.NO_SANDBOX = true;
if (typeof(config.httpSafePort) !== 'number') { if (typeof(Env.httpSafePort) !== 'number') {
config.httpSafePort = config.httpPort + 1; Env.httpSafePort = Env.httpPort + 1;
} }
Env.httpSafeOrigin = deriveSandboxOrigin(Env.httpUnsafeOrigin, Env.httpSafePort);
} }
}()); }());
@ -77,7 +73,7 @@ var setHeaders = (function () {
} }
} else { } else {
// use the default CSP headers constructed with your domain // use the default CSP headers constructed with your domain
headers['Content-Security-Policy'] = Default.contentSecurity(Env.httpUnsafeOrigin); headers['Content-Security-Policy'] = Default.contentSecurity(Env.httpUnsafeOrigin, Env.httpSafeOrigin);
} }
const padHeaders = Util.clone(headers); const padHeaders = Util.clone(headers);
@ -239,14 +235,14 @@ var makeRouteCache = function (template, cacheName) {
var serveConfig = makeRouteCache(function (host) { var serveConfig = makeRouteCache(function (host) {
return [ return [
'define(function(){', 'define(function(){',
'var obj = ' + JSON.stringify({ 'return ' + JSON.stringify({
requireConf: { requireConf: {
waitSeconds: 600, waitSeconds: 600,
urlArgs: 'ver=' + Env.version + cacheString(), urlArgs: 'ver=' + Env.version + cacheString(),
}, },
removeDonateButton: (Env.removeDonateButton === true), removeDonateButton: (Env.removeDonateButton === true),
allowSubscriptions: (Env.allowSubscriptions === true), allowSubscriptions: (Env.allowSubscriptions === true),
websocketPath: config.externalWebsocketURL, websocketPath: Env.websocketPath,
httpUnsafeOrigin: Env.httpUnsafeOrigin, httpUnsafeOrigin: Env.httpUnsafeOrigin,
adminEmail: Env.adminEmail, adminEmail: Env.adminEmail,
adminKeys: Env.admins, adminKeys: Env.admins,
@ -256,16 +252,8 @@ var serveConfig = makeRouteCache(function (host) {
maxUploadSize: Env.maxUploadSize, maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize, premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration, restrictRegistration: Env.restrictRegistration,
httpSafeOrigin: Env.httpSafeOrigin,
}, null, '\t'), }, null, '\t'),
'obj.httpSafeOrigin = ' + (function () {
if (Env.httpSafeOrigin) { return '"' + Env.httpSafeOrigin + '"'; }
if (config.httpSafePort) {
return "(function () { return window.location.origin.replace(/\:[0-9]+$/, ':" +
config.httpSafePort + "'); }())";
}
return 'window.location.origin';
}()),
'return obj',
'});' '});'
].join(';\n') ].join(';\n')
}, 'configCache'); }, 'configCache');
@ -314,11 +302,11 @@ nThen(function (w) {
console.log("CryptPad is customizable, see customize.dist/readme.md for details"); console.log("CryptPad is customizable, see customize.dist/readme.md for details");
})); }));
}).nThen(function (w) { }).nThen(function (w) {
httpServer.listen(config.httpPort,config.httpAddress,function(){ httpServer.listen(Env.httpPort, Env.httpAddress, function(){
var host = config.httpAddress; var host = Env.httpAddress;
var hostName = !host.indexOf(':') ? '[' + host + ']' : host; var hostName = !host.indexOf(':') ? '[' + host + ']' : host;
var port = config.httpPort; var port = Env.httpPort;
var ps = port === 80? '': ':' + port; var ps = port === 80? '': ':' + port;
var roughAddress = 'http://' + hostName + ps; var roughAddress = 'http://' + hostName + ps;
@ -336,8 +324,8 @@ nThen(function (w) {
} }
}); });
if (config.httpSafePort) { if (Env.httpSafePort) {
Http.createServer(app).listen(config.httpSafePort, config.httpAddress, w()); Http.createServer(app).listen(Env.httpSafePort, Env.httpAddress, w());
} }
}).nThen(function () { }).nThen(function () {
var wsConfig = { server: httpServer }; var wsConfig = { server: httpServer };
@ -348,7 +336,7 @@ nThen(function (w) {
config.log = _log; config.log = _log;
if (Env.OFFLINE_MODE) { return; } if (Env.OFFLINE_MODE) { return; }
if (config.externalWebsocketURL) { return; } if (Env.websocketPath) { return; }
require("./lib/api").create(Env); require("./lib/api").create(Env);
}); });

@ -830,6 +830,33 @@ define([
}); });
}); });
assert(function (cb, msg) { // XXX
// check that the sandbox domain is included in connect-src
msg.appendChild(h('span', [
"This instance's ",
code("Content-Security-Policy"),
" headers do not include the sandboxed domain (",
code(trimmedSafe),
") in ",
code("connect-src"),
". This can cause problems with fonts when printing office documents.",
" This is probably due to an incorrectly configured reverse proxy.",
" 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);
},
});
});
/* /*
assert(function (cb, msg) { assert(function (cb, msg) {
setWarningClass(msg); setWarningClass(msg);

Loading…
Cancel
Save