From ef6f98c149ce6e9bb95f8e9ed926cdde6c6d32d7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 13:35:20 +0530 Subject: [PATCH 01/18] initial serverside components for 'disableEmbedding' functionality --- lib/commands/admin-rpc.js | 1 + lib/decrees.js | 5 +++++ server.js | 1 + 3 files changed, 7 insertions(+) diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 59750571e..532deed03 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -300,6 +300,7 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) { var instanceStatus = function (Env, Server, cb) { cb(void 0, { restrictRegistration: Env.restrictRegistration, + disableEmbedding: Env.disableEmbedding, launchTime: Env.launchTime, currentTime: +new Date(), diff --git a/lib/decrees.js b/lib/decrees.js index 5f599705e..c1e417ed5 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -48,6 +48,8 @@ ADD_INVITE REVOKE_INVITE REDEEM_INVITE +DISABLE_EMBEDDING + // 2.0 Env.DEV_MODE || Env.FRESH_MODE, @@ -87,6 +89,9 @@ var makeBooleanSetter = function (attr) { }; }; +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log) +commands.DISABLE_EMBEDDING = makeBooleanSetter('disableEmbedding'); + // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log) commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration'); diff --git a/server.js b/server.js index 73f63a3e1..7d08c3656 100644 --- a/server.js +++ b/server.js @@ -249,6 +249,7 @@ var serveConfig = makeRouteCache(function (host) { premiumUploadSize: Env.premiumUploadSize, restrictRegistration: Env.restrictRegistration, httpSafeOrigin: Env.httpSafeOrigin, + disableEmbedding: Env.disableEmbedding, }, null, '\t'), '});' ].join(';\n') From 96f4162a58d5cc9f90ba0a5597ffeda3ec26a44b Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 13:40:01 +0530 Subject: [PATCH 02/18] initial admin panel components for 'disableEmbedding' functionality --- www/admin/inner.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/www/admin/inner.js b/www/admin/inner.js index 83143a546..8aa6380c4 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -56,6 +56,7 @@ define([ 'cp-admin-archive', 'cp-admin-unarchive', 'cp-admin-registration', + 'cp-admin-disableembeds', 'cp-admin-email' ], 'quota': [ // Msg.admin_cat_quota @@ -302,6 +303,47 @@ define([ return $div; }; + Messages.admin_disableembedsTitle = "Disable remote embedding"; // XXX + Messages.admin_disableembedsHint = "Remove options to embed pads and media-tags hosted on third party websites from sharing menus."; // XXX + //Messages.admin_disableembedsButton = "admin_disableembedsButton"; + Messages.admin_cacheEvictionRequired = "XXX YOU MAY NEED TO USE THE 'FLUSH CACHE' BUTTON FOR THIS TO TAKE EFFECT"; // XXX + + create['disableembeds'] = function () { + var key = 'disableembeds'; + var $div = makeBlock(key); + // Msg.admin_disableembedsHint, .admin_disableembedsTitle, .admin_disableembedsButton + + var state = APP.instanceStatus.disableEmbedding; + var $cbox = $(UI.createCheckbox('cp-settings-' + key, + Messages.admin_disableembedsTitle, + state, { label: { class: 'noTitle' } })); + var spinner = UI.makeSpinner($cbox); + var $checkbox = $cbox.find('input').on('change', function() { + spinner.spin(); + var val = $checkbox.is(':checked') || false; + $checkbox.attr('disabled', 'disabled'); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['DISABLE_EMBEDDING', [val]] + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + } + APP.updateStatus(function () { + spinner.done(); + state = APP.instanceStatus.disableEmbedding; + $checkbox[0].checked = state; + $checkbox.removeAttr('disabled'); + UI.alert(Messages.admin_cacheEvictionRequired); + }); + }); + }); + $cbox.appendTo($div); + + return $div; + }; + var makeAdminCheckbox = function (data) { return function () { var state = data.getState(); From 52529f1a652bb955b4728993097f2a3aee4c5740 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 13:41:08 +0530 Subject: [PATCH 03/18] hide the 'embed' tab of the share menu if 'disableEmbedding' is true --- www/common/inner/share.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/www/common/inner/share.js b/www/common/inner/share.js index 73d736346..aaf4583db 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -1,5 +1,6 @@ define([ 'jquery', + '/api/config', '/common/common-util.js', '/common/common-hash.js', '/common/common-interface.js', @@ -11,7 +12,7 @@ define([ '/customize/messages.js', '/bower_components/nthen/index.js', '/customize/pages.js', -], function ($, Util, Hash, UI, UIElements, Feedback, Modal, h, Clipboard, +], function ($, ApiConfig, Util, Hash, UI, UIElements, Feedback, Modal, h, Clipboard, Messages, nThen, Pages) { var Share = {}; @@ -771,9 +772,9 @@ define([ icon: "fa fa-link", active: !contactsActive, }]; - if (!opts.static) { + if (!opts.static && !ApiConfig.disableEmbedding) { tabs.push({ - getTab: getEmbedTab, + getTab: getEmbedTab, // XXX title: Messages.share_embedCategory, icon: "fa fa-code", onShow: onShowEmbed, @@ -965,11 +966,16 @@ define([ title: Messages.share_linkCategory, icon: "fa fa-link", active: !hasFriends, - }, { - getTab: getFileEmbedTab, - title: Messages.share_embedCategory, - icon: "fa fa-code", }]; + + if (!ApiConfig.disableEmbedding) { + tabs.push({ + getTab: getFileEmbedTab, + title: Messages.share_embedCategory, + icon: "fa fa-code", + }); + } + Modal.getModal(common, opts, tabs, cb); }; From b40c81d08882ed6ac4574c7aeda57a62ab72fced Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 13:54:33 +0530 Subject: [PATCH 04/18] support modifying CSP headers at runtime --- lib/defaults.js | 19 ++++----- lib/env.js | 10 +++++ server.js | 97 ++++++++++++++++++++++++++------------------- www/checkup/main.js | 16 ++++++-- 4 files changed, 88 insertions(+), 54 deletions(-) diff --git a/lib/defaults.js b/lib/defaults.js index 4dd906515..85d3be55e 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,7 +1,8 @@ var Default = module.exports; -Default.commonCSP = function (domain, sandbox) { - domain = ' ' + domain; +Default.commonCSP = function (Env) { + domain = ' ' + Env.httpUnsafeOrigin; + sandbox = Env.httpSafeOrigin; sandbox = (sandbox && sandbox !== domain? ' ' + sandbox: ''); // Content-Security-Policy @@ -31,25 +32,25 @@ Default.commonCSP = function (domain, sandbox) { "media-src blob:", // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox - "frame-ancestors *", + Env.disableEmbedding? `frame-ancestors ${domain}${sandbox}`: "frame-ancestors *", "worker-src 'self'", "" ]; }; -Default.contentSecurity = function (domain, sandbox) { - return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' '); +Default.contentSecurity = function (Env) { + return (Default.commonCSP(Env).join('; ') + "script-src 'self' 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.padContentSecurity = function (Env) { + return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' '); }; -Default.httpHeaders = function () { +Default.httpHeaders = function (Env) { return { "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", - "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Origin": Env.disableEmbedding? '': "*", "Permissions-policy":"interest-cohort=()" }; }; diff --git a/lib/env.js b/lib/env.js index 9970bc4f9..fe2fa4828 100644 --- a/lib/env.js +++ b/lib/env.js @@ -39,9 +39,19 @@ module.exports.create = function (config) { DEV_MODE: false, configCache: {}, broadcastCache: {}, + + officeHeadersCache: undefined, + standardHeadersCache: undefined, + apiHeadersCache: undefined, + flushCache: function () { Env.configCache = {}; Env.broadcastCache = {}; + + Env.officeHeadersCache = undefined; + Env.standardHeadersCache = undefined; + Env.apiHeadersCache = undefined; + Env.FRESH_KEY = +new Date(); if (!(Env.DEV_MODE || Env.FRESH_MODE)) { Env.FRESH_MODE = true; } if (!Env.Log) { return; } diff --git a/server.js b/server.js index 7d08c3656..41b0b016d 100644 --- a/server.js +++ b/server.js @@ -48,9 +48,17 @@ var applyHeaderMap = function (res, map) { for (let header in map) { res.setHeader(header, map[header]); } }; -var setHeaders = (function () { - // load the default http headers unless the admin has provided their own via the config file - var headers; +var EXEMPT = [ + /^\/common\/onlyoffice\/.*\.html.*/, + /^\/(sheet|presentation|doc)\/inner\.html.*/, + /^\/unsafeiframe\/inner\.html.*$/, +]; + +var getHeaders = function (Env, type) { + var key = type + 'HeadersCache'; + if (Env[key]) { return Env[key]; } + + var headers = {}; var custom = config.httpHeaders; // if the admin provided valid http headers then use them @@ -58,11 +66,11 @@ var setHeaders = (function () { headers = Util.clone(custom); } else { // otherwise use the default - headers = Default.httpHeaders(); + headers = Default.httpHeaders(Env); } // next define the base Content Security Policy (CSP) headers - if (typeof(config.contentSecurity) === 'string') { + if (typeof(config.contentSecurity) === 'string') { // XXX deprecate this??? headers['Content-Security-Policy'] = config.contentSecurity; if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' } if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) { @@ -73,47 +81,54 @@ var setHeaders = (function () { } } else { // use the default CSP headers constructed with your domain - headers['Content-Security-Policy'] = Default.contentSecurity(Env.httpUnsafeOrigin, Env.httpSafeOrigin); + headers['Content-Security-Policy'] = Default.contentSecurity(Env); } - const padHeaders = Util.clone(headers); - if (typeof(config.padContentSecurity) === 'string') { - padHeaders['Content-Security-Policy'] = config.padContentSecurity; - } else { - padHeaders['Content-Security-Policy'] = Default.padContentSecurity(Env.httpUnsafeOrigin, Env.httpSafeOrigin); + //const padHeaders = Util.clone(headers); + if (type === 'office') { + if (typeof(config.padContentSecurity) === 'string') { + headers['Content-Security-Policy'] = config.padContentSecurity; // XXX drop support for this + } else { + headers['Content-Security-Policy'] = Default.padContentSecurity(Env); + } + } +/* + headers['Content-Security-Policy'] = type === 'office'? + Default.padContentSecurity(Env): + Default.contentSecurity(Env);*/ + + if (Env.NO_SANDBOX) { // handles correct configuration for local development + // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs + headers["Cross-Origin-Resource-Policy"] = 'cross-origin'; + headers["Cross-Origin-Embedder-Policy"] = 'require-corp'; } - if (Object.keys(headers).length) { - return function (req, res) { - if (Env.NO_SANDBOX) { // handles correct configuration for local development - // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs - applyHeaderMap(res, { - "Cross-Origin-Resource-Policy": 'cross-origin', - "Cross-Origin-Embedder-Policy": 'require-corp', - }); - } - // Don't set CSP headers on /api/ endpoints - // because they aren't necessary and they cause problems - // when duplicated by NGINX in production environments - if (/^\/api\/(broadcast|config)/.test(req.url)) { return; } - - applyHeaderMap(res, { - "Cross-Origin-Resource-Policy": 'cross-origin', - }); - - // targeted CSP, generic policies, maybe custom headers - const h = [ - /^\/common\/onlyoffice\/.*\.html.*/, - /^\/(sheet|presentation|doc)\/inner\.html.*/, - /^\/unsafeiframe\/inner\.html.*$/, - ].some((regex) => { - return regex.test(req.url); - }) ? padHeaders : headers; - applyHeaderMap(res, h); - }; + // Don't set CSP headers on /api/ endpoints + // because they aren't necessary and they cause problems + // when duplicated by NGINX in production environments + if (type === 'api') { + Env[key] = headers; + return headers; } - return function () {}; -}()); + + headers["Cross-Origin-Resource-Policy"] = 'cross-origin'; + Env[key] = headers; + return headers; +}; + +var setHeaders = function (req, res) { + var type; + if (EXEMPT.some(regex => regex.test(req.url))) { + type = 'office'; + } else if (/^\/api\/(broadcast|config)/.test(req.url)) { + type = 'api'; + } else { + type = 'standard'; + } + + var h = getHeaders(Env, type); + applyHeaderMap(res, h); +}; (function () { if (!config.logFeedback) { return; } diff --git a/www/checkup/main.js b/www/checkup/main.js index e7d952dbb..f465283cc 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -978,8 +978,8 @@ define([ 'img-src': ["'self'", 'data:', 'blob:', $outer], 'media-src': ['blob:'], - //'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding - 'worker-src': ["'self'"], // , $outer, $sandbox], + 'frame-ancestors': ApiConfig.disableEmbedding? [$outer, $sandbox]: ['*'], + 'worker-src': ["'self'"], }); cb(result); }); @@ -1016,7 +1016,7 @@ define([ ], 'img-src': ["'self'", 'data:', 'blob:', $outer], 'media-src': ['blob:'], - //'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding + 'frame-ancestors': ApiConfig.disableEmbedding? [$outer, $sandbox]: ['*'], 'worker-src': ["'self'"],//, $outer, $sandbox], }); @@ -1026,7 +1026,7 @@ define([ assert(function (cb, msg) { var header = 'Access-Control-Allow-Origin'; - msg.appendChild(h('span', [ + msg.appendChild(h('span', [ // XXX update text to indicate that the value doesn't match their preference 'Assets must be served with an ', code(header), ' header with a value of ', @@ -1035,6 +1035,14 @@ define([ ])); Tools.common_xhr('/', function (xhr) { var raw = xhr.getResponseHeader(header); + + if (ApiConfig.disableEmbedding) { + if ([null, ''].includes(raw)) { return void cb(true); } + else { + return void cb(raw === '*' || raw); + } + } + cb(raw === "*" || raw); }); }); From fa8e901f541fb247584a1ac9d7db53828bb8f580 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 13:59:00 +0530 Subject: [PATCH 05/18] drop support for 'config.contentSecurity' --- server.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/server.js b/server.js index 41b0b016d..a08a7e5c3 100644 --- a/server.js +++ b/server.js @@ -69,33 +69,9 @@ var getHeaders = function (Env, type) { headers = Default.httpHeaders(Env); } - // next define the base Content Security Policy (CSP) headers - if (typeof(config.contentSecurity) === 'string') { // XXX deprecate this??? - headers['Content-Security-Policy'] = config.contentSecurity; - if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' } - if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) { - // backward compat for those who do not merge the new version of the config - // when updating. This prevents endless spinner if someone clicks donate. - // It also fixes the cross-domain iframe. - headers['Content-Security-Policy'] += "frame-ancestors *;"; - } - } else { - // use the default CSP headers constructed with your domain - headers['Content-Security-Policy'] = Default.contentSecurity(Env); - } - - //const padHeaders = Util.clone(headers); - if (type === 'office') { - if (typeof(config.padContentSecurity) === 'string') { - headers['Content-Security-Policy'] = config.padContentSecurity; // XXX drop support for this - } else { - headers['Content-Security-Policy'] = Default.padContentSecurity(Env); - } - } -/* headers['Content-Security-Policy'] = type === 'office'? Default.padContentSecurity(Env): - Default.contentSecurity(Env);*/ + Default.contentSecurity(Env); if (Env.NO_SANDBOX) { // handles correct configuration for local development // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs From 0917b45035b2a511f58a578785f1edbc70d6a59a Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 16:09:02 +0530 Subject: [PATCH 06/18] implement proper support for forbidding remote media-tag inclusion ...and test that the basic headers are correctly set on the checkup page --- lib/defaults.js | 2 +- lib/env.js | 3 ++- server.js | 4 ++-- www/checkup/main.js | 26 +++++++++++++++++--------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/defaults.js b/lib/defaults.js index 85d3be55e..44cfef5f0 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -50,7 +50,7 @@ Default.httpHeaders = function (Env) { return { "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", - "Access-Control-Allow-Origin": Env.disableEmbedding? '': "*", + "Access-Control-Allow-Origin": Env.disableEmbedding? Env.permittedEmbedders: "*", "Permissions-policy":"interest-cohort=()" }; }; diff --git a/lib/env.js b/lib/env.js index fe2fa4828..f92eae98c 100644 --- a/lib/env.js +++ b/lib/env.js @@ -28,6 +28,8 @@ module.exports.create = function (config) { httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin), httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin), + permittedEmbedders: typeof(config.permittedEmbedders) === 'string'? config.permittedEmbedders: canonicalizeOrigin(config.httpSafeOrigin), + removeDonateButton: config.removeDonateButton, httpPort: isValidPort(config.httpPort)? config.httpPort: 3000, httpAddress: typeof(config.httpAddress) === 'string'? config.httpAddress: '127.0.0.1', @@ -68,7 +70,6 @@ module.exports.create = function (config) { archiveRetentionTime: config.archiveRetentionTime, accountRetentionTime: config.accountRetentionTime, - // TODO implement mutability adminEmail: config.adminEmail, supportMailbox: config.supportMailboxPublicKey, diff --git a/server.js b/server.js index a08a7e5c3..498b7465b 100644 --- a/server.js +++ b/server.js @@ -126,7 +126,7 @@ app.use('/blob', function (req, res, next) { if (req.method === 'HEAD') { Express.static(Path.join(__dirname, Env.paths.blob), { setHeaders: function (res, path, stat) { - res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Origin', Env.disableEmbedding? Env.permittedEmbedders: '*'); res.set('Access-Control-Allow-Headers', 'Content-Length'); res.set('Access-Control-Expose-Headers', 'Content-Length'); } @@ -138,7 +138,7 @@ app.use('/blob', function (req, res, next) { app.use(function (req, res, next) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { - res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Origin', Env.disableEmbedding? Env.permittedEmbedders: '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'); res.setHeader('Access-Control-Max-Age', 1728000); diff --git a/www/checkup/main.js b/www/checkup/main.js index f465283cc..c858e6281 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -1026,23 +1026,31 @@ define([ assert(function (cb, msg) { var header = 'Access-Control-Allow-Origin'; - msg.appendChild(h('span', [ // XXX update text to indicate that the value doesn't match their preference - '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); if (ApiConfig.disableEmbedding) { - if ([null, ''].includes(raw)) { return void cb(true); } + if (raw === trimmedSafe) { return void cb(true); } else { - return void cb(raw === '*' || raw); + msg.appendChild(h('span', [ + 'This instance has been configured to disable support for embedding assets in third-party websites. ', + 'In order for this setting to be effective while still permitting encrypted media to load locally ', + 'the ', + code(header), + ' should only match trusted domains.', + ])); + return void cb(raw); } } + 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.', + ])); + cb(raw === "*" || raw); }); }); From aaa00216d4befcd78263ff31ae6cbec0601c9035 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 18 Feb 2022 16:13:47 +0530 Subject: [PATCH 07/18] add a note about what configurations are supported re: third party embedding --- www/checkup/main.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/checkup/main.js b/www/checkup/main.js index c858e6281..8b2450f19 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -1024,6 +1024,13 @@ define([ }); }); +/* Only two use-cases are currently supported: + 1. remote embedding is enabled, and fully permissive + 2. remote embedding is disabled, so media-tags can only be loaded on your instance + + Support for selectively enabling embedding on remote sites is far more complicated + and will need funding. +*/ assert(function (cb, msg) { var header = 'Access-Control-Allow-Origin'; Tools.common_xhr('/', function (xhr) { From bc365d16a851afe058ca5d96407494080429f3f5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 25 Feb 2022 14:24:28 +0530 Subject: [PATCH 08/18] block loading many apps in iframes --- www/common/sframe-common-outer.js | 34 +++++++++++++++++++++++++++++++ www/debug/main.js | 3 +++ www/login/main.js | 1 + www/register/main.js | 1 + 4 files changed, 39 insertions(+) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 7351a2485..1a380e0a0 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -8,7 +8,41 @@ define([ ], function (nThen, ApiConfig, RequireConfig, Messages, $) { var common = {}; + var embeddableApps = [ + //'calendar', + 'code', + //'doc', // XXX + // 'drive', // XXX + //'file', // doesn't suggest iframes + 'form', + 'kanban', + 'pad', + // 'poll', // XXX + //'presentation', // XXX + // 'sheet', // XXX + 'slide', + //'teams', // XXX + 'whiteboard', + ].map(function (x) { + return `/${x}/`; // XXX intentionally break IE or anything that doesn't support template literals + }); + common.initIframe = function (waitFor, isRt, pathname) { + if (window.top !== window) { + if (ApiConfig.disableEmbedding) { + return void window.alert(`This CryptPad instance's administrators have disabled remote embedding of its editors.`); + } + // even where embedding is not forbidden it should still be limited + // to apps that are explicitly permitted + if (!embeddableApps.includes(window.location.pathname)) { + return void window.alert(`Embedding this CryptPad editor in remote pages is not supported.`); + } + } + + if (window.location.origin !== ApiConfig.httpUnsafeOrigin) { + return void window.alert(`This page is configured to only be accessed via ${ApiConfig.httpUnsafeOrigin}.`); + } + var requireConfig = RequireConfig(); var lang = Messages._languageUsed; var themeKey = 'CRYPTPAD_STORE|colortheme'; diff --git a/www/debug/main.js b/www/debug/main.js index 6f855e1db..124ea39f9 100644 --- a/www/debug/main.js +++ b/www/debug/main.js @@ -13,6 +13,9 @@ define([ '/common/common-interface.js', ], function (nThen, ApiConfig, $, RequireConfig, SFCommonO, Cryptpad, Util, Hash, Realtime, Constants, UI) { + if (window.top !== window) { + return void window.alert(`If you are seeing this message then somebody might be trying to compromise your CryptPad account. Please contact the CryptPad development team.`); + } window.Cryptpad = { Common: Cryptpad, diff --git a/www/login/main.js b/www/login/main.js index df0be1733..1a27343fc 100644 --- a/www/login/main.js +++ b/www/login/main.js @@ -10,6 +10,7 @@ define([ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', ], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore /*, Test */) { + if (window.top !== window) { return; } $(function () { var $checkImport = $('#import-recent'); if (LocalStore.isLoggedIn()) { diff --git a/www/register/main.js b/www/register/main.js index 301306f70..8edbaab92 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -14,6 +14,7 @@ define([ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', ], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h) { + if (window.top !== window) { return; } var Messages = Cryptpad.Messages; $(function () { if (LocalStore.isLoggedIn()) { From a54a0af604850dad55a3b2bc81ae932a2978cbaa Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 14 Mar 2022 17:09:22 +0530 Subject: [PATCH 09/18] more tests on checkup page --- lib/defaults.js | 8 ++++---- lib/env.js | 34 +++++++++++++++++++++++++++++++--- server.js | 33 ++++++++++++++------------------- www/checkup/main.js | 40 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 27 deletions(-) diff --git a/lib/defaults.js b/lib/defaults.js index 44cfef5f0..663d4bd02 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,8 +1,8 @@ var Default = module.exports; Default.commonCSP = function (Env) { - domain = ' ' + Env.httpUnsafeOrigin; - sandbox = Env.httpSafeOrigin; + var domain = ' ' + Env.httpUnsafeOrigin; + var sandbox = Env.httpSafeOrigin; sandbox = (sandbox && sandbox !== domain? ' ' + sandbox: ''); // Content-Security-Policy @@ -39,11 +39,11 @@ Default.commonCSP = function (Env) { }; Default.contentSecurity = function (Env) { - return (Default.commonCSP(Env).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' '); + return (Default.commonCSP(Env).join('; ') + "script-src 'self' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' '); }; Default.padContentSecurity = function (Env) { - return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' '); + return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' '); }; Default.httpHeaders = function (Env) { diff --git a/lib/env.js b/lib/env.js index 5d2855e92..4e718e0c2 100644 --- a/lib/env.js +++ b/lib/env.js @@ -21,14 +21,42 @@ var isValidPort = function (p) { return typeof(p) === 'number' && p < 65535; }; +var deriveSandboxOrigin = function (unsafe, port) { + var url = new URL(unsafe); + url.port = port; + return url.origin; +}; + module.exports.create = function (config) { + var httpUnsafeOrigin = canonicalizeOrigin(config.httpUnsafeOrigin); + + var httpSafeOrigin; + var NO_SANDBOX = false; + var httpSafePort; + var httpPort = isValidPort(config.httpPort)? config.httpPort: 3000; + + if (typeof(config.httpSafeOrigin) !== 'string') { + NO_SANDBOX = true; + if (typeof(config.httpSafePort) !== 'number') { httpSafePort = httpPort + 1; } + httpSafeOrigin = deriveSandboxOrigin(httpUnsafeOrigin, httpSafePort); + } + + var permittedEmbedders = config.permittedEmbedders; + if (typeof(permittedEmbedders) === 'string') { + permittedEmbedders = permittedEmbedders.trim(); + } + const Env = { + fileHost: config.fileHost, // XXX + NO_SANDBOX: NO_SANDBOX, + httpSafePort: httpSafePort, + version: Package.version, installMethod: config.installMethod || undefined, - httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin), - httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin), - permittedEmbedders: typeof(config.permittedEmbedders) === 'string'? config.permittedEmbedders: canonicalizeOrigin(config.httpSafeOrigin), + httpUnsafeOrigin: httpUnsafeOrigin, + httpSafeOrigin: httpSafeOrigin, + permittedEmbedders: typeof(permittedEmbedders) === 'string' && permittedEmbedders? permittedEmbedders: httpSafeOrigin, removeDonateButton: config.removeDonateButton, httpPort: isValidPort(config.httpPort)? config.httpPort: 3000, diff --git a/server.js b/server.js index 3e6da369f..73e4d2b00 100644 --- a/server.js +++ b/server.js @@ -23,29 +23,17 @@ 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') { + if (typeof(Env.httpUnsafeOrigin) !== 'string' || !Env.httpUnsafeOrigin.trim()) { throw new Error("No 'httpUnsafeOrigin' provided"); } - - if (typeof(Env.httpSafeOrigin) !== 'string') { - Env.NO_SANDBOX = true; - if (typeof(Env.httpSafePort) !== 'number') { - Env.httpSafePort = Env.httpPort + 1; - } - Env.httpSafeOrigin = deriveSandboxOrigin(Env.httpUnsafeOrigin, Env.httpSafePort); - } }()); var applyHeaderMap = function (res, map) { - for (let header in map) { res.setHeader(header, map[header]); } + for (let header in map) { + if (typeof(map[header]) === 'string') { res.setHeader(header, map[header]); } + } }; var EXEMPT = [ @@ -54,6 +42,11 @@ var EXEMPT = [ /^\/unsafeiframe\/inner\.html.*$/, ]; +var cacheHeaders = function (Env, key, headers) { + if (Env.DEV_MODE) { return; } + Env[key] = headers; +}; + var getHeaders = function (Env, type) { var key = type + 'HeadersCache'; if (Env[key]) { return Env[key]; } @@ -83,12 +76,12 @@ var getHeaders = function (Env, type) { // because they aren't necessary and they cause problems // when duplicated by NGINX in production environments if (type === 'api') { - Env[key] = headers; + cacheHeaders(Env, key, headers); return headers; } headers["Cross-Origin-Resource-Policy"] = 'cross-origin'; - Env[key] = headers; + cacheHeaders(Env, key, headers); return headers; }; @@ -103,6 +96,7 @@ var setHeaders = function (req, res) { } var h = getHeaders(Env, type); + //console.log('PEWPEW', type, h); applyHeaderMap(res, h); }; @@ -140,7 +134,7 @@ app.use(function (req, res, next) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { res.setHeader('Access-Control-Allow-Origin', Env.disableEmbedding? Env.permittedEmbedders: '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'); + res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Access-Control-Allow-Origin'); res.setHeader('Access-Control-Max-Age', 1728000); res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8'); res.setHeader('Content-Length', 0); @@ -241,6 +235,7 @@ var serveConfig = makeRouteCache(function (host) { restrictRegistration: Env.restrictRegistration, httpSafeOrigin: Env.httpSafeOrigin, disableEmbedding: Env.disableEmbedding, + fileHost: Env.fileHost, }, null, '\t'), '});' ].join(';\n') diff --git a/www/checkup/main.js b/www/checkup/main.js index 5a17eaf2c..039b27183 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -1045,7 +1045,10 @@ define([ code(header), ' should only match trusted domains.', ])); - return void cb(raw); + return void cb({ + header: raw, + expected: trimmedSafe, + }); } } @@ -1271,6 +1274,41 @@ define([ }); }); + assert(function (cb, msg) { + var url; + try { + url = new URL('/', trimmedUnsafe); + } catch (err) { + return void cb({ + error: err, + }); + } + + // XXX don't bother checking cors headers in dev environment + if (url.protocol !== 'https') { return void cb(true); } // XXX + + var header = 'Access-Control-Allow-Origin'; + msg.appendChild(h('span', [ + 'pewpew ', + code(header), // XXX + ])); + + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url.href, + header: header, + }, + }, function (raw) { + if (raw === '*') { return void cb(true); } + if (raw === trimmedSafe) { return void cb(true); } + cb({ + response: raw, + disableEmbedding: ApiConfig.disableEmbedding, + }); + }); + }); + var serverToken; Tools.common_xhr('/', function (xhr) { serverToken = xhr.getResponseHeader('server'); From 1051fc4da7c81c94ef67a0278edeb7f905b28f08 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 14 Mar 2022 17:43:51 +0530 Subject: [PATCH 10/18] fix undefined safe origin --- lib/env.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/env.js b/lib/env.js index 4e718e0c2..4262e3fbf 100644 --- a/lib/env.js +++ b/lib/env.js @@ -39,6 +39,8 @@ module.exports.create = function (config) { NO_SANDBOX = true; if (typeof(config.httpSafePort) !== 'number') { httpSafePort = httpPort + 1; } httpSafeOrigin = deriveSandboxOrigin(httpUnsafeOrigin, httpSafePort); + } else { + httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin); } var permittedEmbedders = config.permittedEmbedders; From e1abf4ef7722f8eca9214b4db770b945213ff2a1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 14 Mar 2022 18:23:38 +0530 Subject: [PATCH 11/18] nginx updates --- docs/example.nginx.conf | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 13fd7789f..be56e10cd 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -19,6 +19,16 @@ server { set $main_domain "your-main-domain.com"; set $sandbox_domain "your-sandbox-domain.com"; + # By default CryptPad allows remote domains to embed CryptPad documents in iframes. + # This behaviour can be blocked by changing $allowed_origins from "*" to the + # sandbox domain, which must be permitted to load content from the main domain + # in order for CryptPad to work as expected. + # + # An example is given below which can be uncommented if you want to block + # remote sites from including content from your server + set $allowed_origins "*"; + # set $allowed_origins "https://${sandbox_domain}"; + # CryptPad's dynamic content (websocket traffic and encrypted blobs) # can be served over separate domains. Using dedicated domains (or subdomains) # for these purposes allows you to move them to a separate machine at a later date @@ -58,7 +68,7 @@ server { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options nosniff; - add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Origin "${allowed_origins}"; # add_header X-Frame-Options "SAMEORIGIN"; # Opt out of Google's FLoC Network @@ -115,6 +125,9 @@ server { # script-src specifies valid sources for javascript, including inline handlers set $scriptSrc "'self' resource: https://${main_domain}"; + # XXX frame-ancestors defines where your cryptpad instance can be embedded... + set $frameAncestors "https://${main_domain} $https://${sandbox_domain}"; + set $unsafe 0; # the following assets are loaded via the sandbox domain # they unfortunately still require exceptions to the sandboxing to work correctly. @@ -135,7 +148,7 @@ server { } # Finally, set all the rules you composed above. - add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;"; + add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors"; # The nodejs process can handle all traffic whether accessed over websocket or as static assets # We prefer to serve static content from nginx directly and to leave the API server to handle @@ -183,7 +196,7 @@ server { # encrypted blobs are immutable and are thus cached for a year location ^~ /blob/ { if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Origin' "${allowed_origins}"; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Max-Age' 1728000; @@ -192,7 +205,7 @@ server { return 204; } add_header Cache-Control max-age=31536000; - add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Origin' "${allowed_origins}"; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length'; add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length'; From 26fcda633e12dc0d74d69a14a92673a21d00836d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 12:00:41 +0530 Subject: [PATCH 12/18] restrict activities performed in a nested context --- www/common/common-ui-elements.js | 10 ++++++++++ www/common/sframe-app-framework.js | 4 ++++ www/common/sframe-common-outer.js | 18 ++++++++++++------ www/common/sframe-common.js | 3 +++ www/common/toolbar.js | 8 +++++++- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 01e19f0ef..154fd1d6e 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -523,6 +523,16 @@ define([ UI.openCustomModal(modal); }; + Messages.ui_openDirectly = "This functionality is not available when CryptPad is embedded in another site. Open this pad in its own window?";// XXX + UIElements.openDirectlyConfirmation = function (common, cb) { + cb = cb || Util.noop; + UI.confirm(h('p', Messages.ui_openDirectly), yes => { + if (!yes) { return void cb(yes); } + common.openDirectly(); + cb(yes); + }); + }; + UIElements.createButton = function (common, type, rightside, data, callback) { var AppConfig = common.getAppConfig(); var button; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index e0962daf7..04821ce15 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -734,6 +734,10 @@ define([ var createFilePicker = function () { if (!common.isLoggedIn()) { return; } $embedButton = common.createButton('mediatag', true).click(function () { + if (!cpNfInner.metadataMgr.getPrivateData().isTop) { + return void UIElements.openDirectlyConfirmation(common); + } + var cfg = { types: ['file', 'link'], where: ['root'] diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 91d11c20d..1dd30afa1 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -641,6 +641,7 @@ define([ prefersDriveRedirect: Utils.LocalStore.getDriveRedirectPreference(), isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, + isTop: window.top === window, canEdit: hashes && hashes.editHash, oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, @@ -838,14 +839,19 @@ define([ } }); - sframeChan.on('EV_OPEN_URL', function (url) { - if (url) { - var a = window.open(url); - if (!a) { - sframeChan.event('EV_POPUP_BLOCKED'); - } + var openURL = function (url) { + if (!url) { return; } + var a = window.open(url); + if (!a) { + sframeChan.event('EV_POPUP_BLOCKED'); } + }; + + sframeChan.on('EV_OPEN_URL_DIRECTLY', function () { + var url = currentPad.href; + openURL(url); }); + sframeChan.on('EV_OPEN_URL', openURL); sframeChan.on('EV_OPEN_UNSAFE_URL', function (url) { if (url) { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 8f97ba265..66977d664 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -662,6 +662,9 @@ define([ }); }; + funcs.openDirectly = function () { + ctx.sframeChan.event('EV_OPEN_URL_DIRECTLY'); + }; funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); }; funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); }; funcs.getBounceURL = function (url) { diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8931c57f5..11772ca06 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -582,6 +582,9 @@ MessengerUI, Messages, Pages) { hidden: true }); $shareBlock.click(function () { + if (!config.metadataMgr.getPrivateData().isTop) { + return void UIElements.openDirectlyConfirmation(Common); + } if (toolbar.isDeleted) { return void UI.warn(Messages.deletedFromServer); } @@ -609,7 +612,10 @@ MessengerUI, Messages, Pages) { h('i.fa.fa-unlock-alt'), h('span.cp-button-name', Messages.accessButton) ])); - $accessBlock.click(function () { + $accessBlock.click(function () { + if (!config.metadataMgr.getPrivateData().isTop) { + return void UIElements.openDirectlyConfirmation(Common); + } if (toolbar.isDeleted) { return void UI.warn(Messages.deletedFromServer); } From b83e8600f4df7811fd4f1d6e8015b31b8f6e20ee Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 13:35:49 +0530 Subject: [PATCH 13/18] clean up checkup tests and remove XXX --- www/checkup/main.js | 79 ++++++++++++++++++++------------------- www/common/inner/share.js | 2 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 039b27183..76f6b88d7 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -1030,37 +1030,46 @@ define([ Support for selectively enabling embedding on remote sites is far more complicated and will need funding. */ - assert(function (cb, msg) { + var checkAllowedOrigins = function (raw, url, msg, cb) { var header = 'Access-Control-Allow-Origin'; - Tools.common_xhr('/', function (xhr) { - var raw = xhr.getResponseHeader(header); - - if (ApiConfig.disableEmbedding) { - if (raw === trimmedSafe) { return void cb(true); } - else { - msg.appendChild(h('span', [ - 'This instance has been configured to disable support for embedding assets in third-party websites. ', - 'In order for this setting to be effective while still permitting encrypted media to load locally ', - 'the ', - code(header), - ' should only match trusted domains.', - ])); - return void cb({ - header: raw, - expected: trimmedSafe, - }); - } - } - + var expected; + if (ApiConfig.disableEmbedding) { + expected = trimmedSafe; + msg.appendChild(h('span', [ + 'This instance has been configured to disable support for embedding assets and documents in third-party websites. ', + 'In order for this setting to be effective while still permitting encrypted media to load locally ', + 'the ', + code(header), + ' should only match trusted domains.', + ' Under most circumstances it is sufficient to permit only the sandbox domain to load assets.', + " Remote embedding can be enabled via the admin panel.", + ])); + } else { + expected = '*'; msg.appendChild(h('span', [ + "This instance has been configured to permit embedding assets and documents in third-party websites.", '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.', + '.', + ' Remote embedding can be disabled via the admin panel.', ])); + } + if (raw === expected) { return void cb(true); } + cb({ + url: url, + response: raw, + disableEmbedding: ApiConfig.disableEmbedding, + }); + }; - cb(raw === "*" || raw); + assert(function (cb, msg) { + var header = 'Access-Control-Allow-Origin'; + var url = new URL('/', trimmedUnsafe).href; + Tools.common_xhr(url, function (xhr) { + var raw = xhr.getResponseHeader(header); + checkAllowedOrigins(raw, url, msg, cb); }); }); @@ -1279,20 +1288,17 @@ define([ try { url = new URL('/', trimmedUnsafe); } catch (err) { - return void cb({ - error: err, - }); + // if your configuration is bad enough that this throws + // then other tests should detect it. Let's just bail out + return void cb(true); } - // XXX don't bother checking cors headers in dev environment - if (url.protocol !== 'https') { return void cb(true); } // XXX + // xhr.getResponseHeader and similar APIs don't behave as expected in insecure cross-origin contexts + // which prevents us from inspecting headers in a development context. We bail out early + // and assume it passed. The proper test will run as normal in production + if (url.protocol !== 'https') { return void cb(true); } var header = 'Access-Control-Allow-Origin'; - msg.appendChild(h('span', [ - 'pewpew ', - code(header), // XXX - ])); - deferredPostMessage({ command: 'GET_HEADER', content: { @@ -1300,12 +1306,7 @@ define([ header: header, }, }, function (raw) { - if (raw === '*') { return void cb(true); } - if (raw === trimmedSafe) { return void cb(true); } - cb({ - response: raw, - disableEmbedding: ApiConfig.disableEmbedding, - }); + checkAllowedOrigins(raw, url.href, msg, cb); }); }); diff --git a/www/common/inner/share.js b/www/common/inner/share.js index aaf4583db..eeea74e7f 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -774,7 +774,7 @@ define([ }]; if (!opts.static && !ApiConfig.disableEmbedding) { tabs.push({ - getTab: getEmbedTab, // XXX + getTab: getEmbedTab, title: Messages.share_embedCategory, icon: "fa fa-code", onShow: onShowEmbed, From 87f6f97ccac26baccc177180a3e50f5cd121ab9a Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 15:28:20 +0530 Subject: [PATCH 14/18] prompt admins to flush cache when relevant also remove some accidentally duplicated code --- www/admin/inner.js | 115 +++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 78 deletions(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index 29ee935cf..d7853c814 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -294,81 +294,6 @@ define([ return $div; }; - create['registration'] = function () { - var key = 'registration'; - var $div = makeBlock(key); // Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton - - var state = APP.instanceStatus.restrictRegistration; - var $cbox = $(UI.createCheckbox('cp-settings-' + key, - Messages.admin_registrationTitle, - state, { label: { class: 'noTitle' } })); - var spinner = UI.makeSpinner($cbox); - var $checkbox = $cbox.find('input').on('change', function() { - spinner.spin(); - var val = $checkbox.is(':checked') || false; - $checkbox.attr('disabled', 'disabled'); - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['RESTRICT_REGISTRATION', [val]] - }, function (e, response) { - if (e || response.error) { - UI.warn(Messages.error); - console.error(e, response); - } - APP.updateStatus(function () { - spinner.done(); - state = APP.instanceStatus.restrictRegistration; - $checkbox[0].checked = state; - $checkbox.removeAttr('disabled'); - }); - }); - }); - $cbox.appendTo($div); - - return $div; - }; - - Messages.admin_disableembedsTitle = "Disable remote embedding"; // XXX - Messages.admin_disableembedsHint = "Remove options to embed pads and media-tags hosted on third party websites from sharing menus."; // XXX - //Messages.admin_disableembedsButton = "admin_disableembedsButton"; - Messages.admin_cacheEvictionRequired = "XXX YOU MAY NEED TO USE THE 'FLUSH CACHE' BUTTON FOR THIS TO TAKE EFFECT"; // XXX - - create['disableembeds'] = function () { - var key = 'disableembeds'; - var $div = makeBlock(key); - // Msg.admin_disableembedsHint, .admin_disableembedsTitle, .admin_disableembedsButton - - var state = APP.instanceStatus.disableEmbedding; - var $cbox = $(UI.createCheckbox('cp-settings-' + key, - Messages.admin_disableembedsTitle, - state, { label: { class: 'noTitle' } })); - var spinner = UI.makeSpinner($cbox); - var $checkbox = $cbox.find('input').on('change', function() { - spinner.spin(); - var val = $checkbox.is(':checked') || false; - $checkbox.attr('disabled', 'disabled'); - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['DISABLE_EMBEDDING', [val]] - }, function (e, response) { - if (e || response.error) { - UI.warn(Messages.error); - console.error(e, response); - } - APP.updateStatus(function () { - spinner.done(); - state = APP.instanceStatus.disableEmbedding; - $checkbox[0].checked = state; - $checkbox.removeAttr('disabled'); - UI.alert(Messages.admin_cacheEvictionRequired); - }); - }); - }); - $cbox.appendTo($div); - - return $div; - }; - var makeAdminCheckbox = function (data) { return function () { var state = data.getState(); @@ -396,8 +321,17 @@ define([ }; }; - // Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton - create['registration'] = makeAdminCheckbox({ + Messages.admin_cacheEvictionRequired = "Your server's internal state has been updated, but you may need to use the 'flush cache' button for clients to experience the intended effect."; // XXX + Messages.admin_reviewCheckupNotice = "It is also recommended that you review this instance's checkup page to confirm that it is configured correctly."; // XXX + var flushCacheNotice = function () { + UI.alert(h('span', [ + h('p', Messages.admin_cacheEvictionRequired), + h('p', Messages.admin_reviewCheckupNotice), + ])); + }; + + // Msg.admin_registrationHint, .admin_registrationTitle + create['registration'] = makeAdminCheckbox({ // XXX key: 'registration', getState: function () { return APP.instanceStatus.restrictRegistration; @@ -413,6 +347,32 @@ define([ } APP.updateStatus(function () { setState(APP.instanceStatus.restrictRegistration); + flushCacheNotice(); + }); + }); + }, + }); + + Messages.admin_disableembedsTitle = "Disable remote embedding"; // XXX + Messages.admin_disableembedsHint = "Remove options to embed pads and media-tags hosted on third party websites from sharing menus."; // XXX + // Msg.admin_disableembedsHint, .admin_disableembedsTitle + create['disableembeds'] = makeAdminCheckbox({ + key: 'disableembeds', + getState: function () { + return APP.instanceStatus.disableEmbedding; + }, + query: function (val, setState) { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['DISABLE_EMBEDDING', [val]] + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + } + APP.updateStatus(function () { + setState(APP.instanceStatus.disableEmbedding); + flushCacheNotice(); }); }); }, @@ -1999,7 +1959,6 @@ define([ Messages.admin_bytesWrittenTitle = "Disk performance measurement window"; // XXX Messages.admin_bytesWrittenHint = "If you have enabled disk performance measurements then the duration of the window can be configured below."; // XXX Messages.admin_bytesWrittenDuration = "Duration of the window in milliseconds: {0}"; // XXX - //Messages.admin_defaultDuration = "admin_defaultDuration"; // XXX Messages.admin_setDuration = "Set duration"; // XXX var isPositiveInteger = function (n) { From d405a5f086e1098a30db4eb3089a838e0fcf84de Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 15:29:18 +0530 Subject: [PATCH 15/18] disable remote embedding by default --- lib/env.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/env.js b/lib/env.js index 4262e3fbf..64280193b 100644 --- a/lib/env.js +++ b/lib/env.js @@ -153,6 +153,9 @@ module.exports.create = function (config) { } }, + // as of 4.14.0 you need to opt-in to remote embedding. + disableEmbedding: true, + /* FIXME restrictRegistration is initialized as false and then overridden by admin decree There is a narrow window in which someone could register before the server updates this value. See also the cached 'restrictRegistration' value in server.js#serveConfig From 67303d6a9279cca5d696e6cd6764b54f96a9f89d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 16:06:34 +0530 Subject: [PATCH 16/18] hide embed tab of share modal for docs that don't support embedding --- www/common/inner/share.js | 11 ++++++++++- www/common/sframe-common-outer.js | 10 +--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/www/common/inner/share.js b/www/common/inner/share.js index eeea74e7f..7e2f0a6b0 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -16,6 +16,15 @@ define([ Messages, nThen, Pages) { var Share = {}; + var embeddableApps = [ + 'code', + 'form', + 'kanban', + 'pad', + 'slide', + 'whiteboard', + ].map(app => `/${app}/`); + var createShareWithFriends = function (config, onShare, linkGetter) { var common = config.common; var sframeChan = common.getSframeChannel(); @@ -772,7 +781,7 @@ define([ icon: "fa fa-link", active: !contactsActive, }]; - if (!opts.static && !ApiConfig.disableEmbedding) { + if (!opts.static && !ApiConfig.disableEmbedding && embeddableApps.includes(pathname)) { tabs.push({ getTab: getEmbedTab, title: Messages.share_embedCategory, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 87cfa6f7e..ed6962328 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -9,22 +9,14 @@ define([ var common = {}; var embeddableApps = [ - //'calendar', 'code', - //'doc', // XXX - // 'drive', // XXX - //'file', // doesn't suggest iframes 'form', 'kanban', 'pad', - // 'poll', // XXX - //'presentation', // XXX - // 'sheet', // XXX 'slide', - //'teams', // XXX 'whiteboard', ].map(function (x) { - return `/${x}/`; // XXX intentionally break IE or anything that doesn't support template literals + return `/${x}/`; }); common.initIframe = function (waitFor, isRt, pathname) { From 52b423e9ba59cd3a9e59e02307bf20e321aceeb6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 15 Mar 2022 16:20:22 +0530 Subject: [PATCH 17/18] remove a temporary comment and a hardcoded translation --- www/admin/inner.js | 2 +- www/common/common-ui-elements.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index d7853c814..5349a49b6 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -331,7 +331,7 @@ define([ }; // Msg.admin_registrationHint, .admin_registrationTitle - create['registration'] = makeAdminCheckbox({ // XXX + create['registration'] = makeAdminCheckbox({ key: 'registration', getState: function () { return APP.instanceStatus.restrictRegistration; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 154fd1d6e..5661ade7f 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -523,7 +523,6 @@ define([ UI.openCustomModal(modal); }; - Messages.ui_openDirectly = "This functionality is not available when CryptPad is embedded in another site. Open this pad in its own window?";// XXX UIElements.openDirectlyConfirmation = function (common, cb) { cb = cb || Util.noop; UI.confirm(h('p', Messages.ui_openDirectly), yes => { From 1a247af0a982863d87b7c5503dbda54ea3e1538f Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 21 Mar 2022 17:48:41 +0530 Subject: [PATCH 18/18] block access to the properties menu in iframes --- www/common/common-ui-elements.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 5661ade7f..422820361 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -888,6 +888,14 @@ define([ .text(Messages.propertiesButton)) .click(common.prepareFeedback(type)) .click(function () { + var isTop; + try { + isTop = common.getMetadataMgr().getPrivateData().isTop; + } catch (err) { console.error(err); } + if (!isTop) { + return void UIElements.openDirectlyConfirmation(common); + } + sframeChan.event('EV_PROPERTIES_OPEN'); }); break;