diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index ea21e3ba7..6a9d268e8 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -96,14 +96,14 @@ server { set $fontSrc "'self' data: ${main_domain}"; # images can be loaded from anywhere, though we'd like to deprecate this as it allows the use of images for tracking - set $imgSrc "'self' data: * blob: ${main_domain}"; + set $imgSrc "'self' data: blob: ${main_domain} ${sandbox_domain}"; # frame-src specifies valid sources for nested browsing contexts. # this prevents loading any iframes from anywhere other than the sandbox domain set $frameSrc "'self' ${sandbox_domain} blob:"; # specifies valid sources for loading media using video or audio - set $mediaSrc "'self' data: * blob: ${main_domain}"; + set $mediaSrc "'self' data: blob: ${main_domain} ${sandbox_domain}"; # defines valid sources for webworkers and nested browser contexts # deprecated in favour of worker-src and frame-src diff --git a/lib/defaults.js b/lib/defaults.js index c4cb507cb..f43253ccb 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -2,6 +2,7 @@ var Default = module.exports; Default.commonCSP = function (domain, sandbox) { domain = ' ' + domain; + sandbox = (sandbox && sandbox !== domain? ' ' + sandbox: ''); // Content-Security-Policy return [ @@ -15,19 +16,19 @@ Default.commonCSP = function (domain, sandbox) { * it is recommended that you configure these fields to match the * domain which will serve your CryptPad instance. */ - "child-src blob: *", + "child-src 'self' blob: " + domain + sandbox, // IE/Edge - "frame-src blob: *", + "frame-src 'self' blob: " + domain + sandbox, /* this allows connections over secure or insecure websockets 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 + (sandbox && sandbox !== domain? ' ' + sandbox: ''), + "connect-src 'self' ws: wss: blob: " + domain + sandbox, // data: is used by codemirror "img-src 'self' data: blob:" + domain, - "media-src * blob:", + "media-src blob:", // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox "frame-ancestors *", diff --git a/www/bounce/main.js b/www/bounce/main.js index ddb23d983..3b955efc0 100644 --- a/www/bounce/main.js +++ b/www/bounce/main.js @@ -1,20 +1,81 @@ define(['/api/config'], function (ApiConfig) { +/* The 'bounce app' provides a unified way to do the following things in CryptPad + + 1. remove the 'opener' attribute from the tab/window every time you navigate + 2. detect and block malicious URLs after warning the user + 3. inform users when they are navigating away from their cryptpad instance + +*/ + + // when a URL is rejected we close the window + var reject = function () { + window.close(); + }; + // this app is intended to be loaded and used exclusively from the sandbox domain + // where stricter CSP blocks various attacks. Reject any other usage. if (ApiConfig.httpSafeOrigin !== window.location.origin) { window.alert('The bounce application must only be used from the sandbox domain, ' + 'please report this issue on https://github.com/xwiki-labs/cryptpad'); - return; + return void reject(); } - var bounceTo = decodeURIComponent(window.location.hash.slice(1)); - if (!bounceTo) { - window.alert('The bounce application must only be used with a valid href to visit'); - return; - } - if (bounceTo.indexOf('javascript:') === 0 || // jshint ignore:line - bounceTo.indexOf('vbscript:') === 0 || // jshint ignore:line - bounceTo.indexOf('data:') === 0) { - window.alert('Illegal bounce URL'); - return; + // Old/bad browsers lack the URL API, making it more difficult to validate and compare URLs. + // Warn and reject. + if (typeof(URL) !== 'function') { + window.alert("Your browser does not support functionality this page requires"); + return void reject(); } + + // remove the 'opener' to prevent 'reverse tabnabbing'. window.opener = null; - window.location.href = bounceTo; + + // Parse the outer domain's root URL to facilitate comparisons. + // Reject everything if this fails to parse. + var host; + try { + host = new URL('', ApiConfig.httpUnsafeOrigin); + } catch (err) { + window.alert("This server is configured incorrectly. Details for its administrator can be found on its diagnostics page."); + return void reject(); + } + + // Decode the target URL that should have been provided through the document's hash. + // Reject if no URL was provided. + // Absolute URLs are easy to handle, other consider URLs relative to the outer domain. + var target; + try { + var bounceTo = decodeURIComponent(window.location.hash.slice(1)); + target = new URL(bounceTo, ApiConfig.httpUnsafeOrigin); + } catch (err) { + console.error(err); + window.alert('The bounce application must only be used with a valid href to visit'); + return void reject(); + } + + // Valid links should navigate to the normalized href + var go = function () { + window.location.href = target.href; + }; + + // Local URLs don't require any warning and can navigate directly without user input. + if (target.host === host.host) { return void go(); } + + // Everything else requires user input, so we load the platform's translations. + // FIXME: this seems to infer language preferences from the browser instead of the user's account preferences + require([ + '/customize/messages.js', + ], function (Messages) { + // The provided URL seems to be a malicious or invalid payload. + // Inform the user that we won't navigate and that the 'bounce tab' will be closed. + if (['javascript:', 'vbscript:', 'data:', 'blob:'].includes(target.protocol)) { + window.alert(Messages._getKey('bounce_danger', [target.href])); + return void reject(); + } + + // The provided URL will navigate the user away from the outer domain. + var question = Messages._getKey('bounce_confirm', [host.hostname, target.href]); + // Confirm that they want to leave, then navigate or reject based on their choice. + var answer = window.confirm(question); + if (answer) { return void go(); } + reject(); + }); }); diff --git a/www/checkup/main.js b/www/checkup/main.js index d710747d7..f490014c1 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -859,6 +859,43 @@ define([ }); }); + assert(function (cb, msg) { + var directives = [ + 'img-src', + 'media-src', + 'child-src', + 'frame-src' + ]; + + msg.appendChild(h('span', [ + "This instance's ", + code("Content-Security-Policy"), + " headers are unnecessarily permissive.", + h('br'), + 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('/'), { + 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); + }, + }); + }); + /* assert(function (cb, msg) { setWarningClass(msg);