diff --git a/lib/defaults.js b/lib/defaults.js index 635e155be..c4cb507cb 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,6 +1,6 @@ var Default = module.exports; -Default.commonCSP = function (domain) { +Default.commonCSP = function (domain, sandbox) { domain = ' ' + domain; // Content-Security-Policy @@ -23,7 +23,7 @@ Default.commonCSP = function (domain) { if you are deploying to production, you'll probably want to remove 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 "img-src 'self' data: blob:" + domain, @@ -35,12 +35,12 @@ Default.commonCSP = function (domain) { ]; }; -Default.contentSecurity = function (domain) { - return (Default.commonCSP(domain).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' '); +Default.contentSecurity = function (domain, sandbox) { + return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' '); }; -Default.padContentSecurity = function (domain) { - return (Default.commonCSP(domain).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' '); +Default.padContentSecurity = function (domain, sandbox) { + return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' '); }; Default.httpHeaders = function () { diff --git a/lib/env.js b/lib/env.js index 5ad0bbdac..9970bc4f9 100644 --- a/lib/env.js +++ b/lib/env.js @@ -17,6 +17,10 @@ var canonicalizeOrigin = function (s) { return (s || '').trim().replace(/\/+$/, ''); }; +var isValidPort = function (p) { + return typeof(p) === 'number' && p < 65535; +}; + module.exports.create = function (config) { const Env = { version: Package.version, @@ -25,6 +29,9 @@ module.exports.create = function (config) { httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin), httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin), 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, FRESH_KEY: '', diff --git a/server.js b/server.js index 07cae4b23..0b95119f9 100644 --- a/server.js +++ b/server.js @@ -23,28 +23,24 @@ var fancyURL = function (domain, path) { return false; }; +var deriveSandboxOrigin = function (unsafe, port) { + var url = new URL(unsafe); + url.port = port; + return url.origin; +}; + (function () { // you absolutely must provide an 'httpUnsafeOrigin' (a truthy string) if (!Env.httpUnsafeOrigin || typeof(Env.httpUnsafeOrigin) !== 'string') { 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') { Env.NO_SANDBOX = true; - if (typeof(config.httpSafePort) !== 'number') { - config.httpSafePort = config.httpPort + 1; + if (typeof(Env.httpSafePort) !== 'number') { + Env.httpSafePort = Env.httpPort + 1; } + Env.httpSafeOrigin = deriveSandboxOrigin(Env.httpUnsafeOrigin, Env.httpSafePort); } }()); @@ -77,7 +73,7 @@ var setHeaders = (function () { } } else { // 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); @@ -239,14 +235,14 @@ var makeRouteCache = function (template, cacheName) { var serveConfig = makeRouteCache(function (host) { return [ 'define(function(){', - 'var obj = ' + JSON.stringify({ + 'return ' + JSON.stringify({ requireConf: { waitSeconds: 600, urlArgs: 'ver=' + Env.version + cacheString(), }, removeDonateButton: (Env.removeDonateButton === true), allowSubscriptions: (Env.allowSubscriptions === true), - websocketPath: config.externalWebsocketURL, + websocketPath: Env.websocketPath, httpUnsafeOrigin: Env.httpUnsafeOrigin, adminEmail: Env.adminEmail, adminKeys: Env.admins, @@ -256,16 +252,8 @@ var serveConfig = makeRouteCache(function (host) { maxUploadSize: Env.maxUploadSize, premiumUploadSize: Env.premiumUploadSize, restrictRegistration: Env.restrictRegistration, + httpSafeOrigin: Env.httpSafeOrigin, }, 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') }, 'configCache'); @@ -314,11 +302,11 @@ nThen(function (w) { console.log("CryptPad is customizable, see customize.dist/readme.md for details"); })); }).nThen(function (w) { - httpServer.listen(config.httpPort,config.httpAddress,function(){ - var host = config.httpAddress; + httpServer.listen(Env.httpPort, Env.httpAddress, function(){ + var host = Env.httpAddress; var hostName = !host.indexOf(':') ? '[' + host + ']' : host; - var port = config.httpPort; + var port = Env.httpPort; var ps = port === 80? '': ':' + port; var roughAddress = 'http://' + hostName + ps; @@ -336,8 +324,8 @@ nThen(function (w) { } }); - if (config.httpSafePort) { - Http.createServer(app).listen(config.httpSafePort, config.httpAddress, w()); + if (Env.httpSafePort) { + Http.createServer(app).listen(Env.httpSafePort, Env.httpAddress, w()); } }).nThen(function () { var wsConfig = { server: httpServer }; @@ -348,7 +336,7 @@ nThen(function (w) { config.log = _log; if (Env.OFFLINE_MODE) { return; } - if (config.externalWebsocketURL) { return; } + if (Env.websocketPath) { return; } require("./lib/api").create(Env); }); diff --git a/www/checkup/main.js b/www/checkup/main.js index 3118be749..d3841b8bd 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -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) { setWarningClass(msg);