diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e28e69132 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,45 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Where did it happen?** +Did the issue occur on CryptPad.fr or an instance hosted by a third-party? +If on another instance, please provide its full URL. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Browser (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. firefox, tor browser, chrome, safari, brave, edge, ???] + - variations [e.g. Firefox nightly, Firefox ESR, Chromium, Ungoogled chrome] + - Version [e.g. 22] + - Extensions installed (UBlock Origin, Passbolt, LibreJS] + - Browser tweaks [e.g. firefox "Enhanced Tracking Protection" strict/custom mode, tor browser "safer" security level, chrome incognito mode] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.gitignore b/.gitignore index d96f6e6ac..50796e9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ block/ logs/ privileged.conf config/config.js - +*yolo.sh diff --git a/config/config.example.js b/config/config.example.js index 25a4c97a6..273c196d2 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -1,67 +1,110 @@ -/* - globals module -*/ -var _domain = 'http://localhost:3000/'; - -// You can `kill -USR2` the node process and it will write out a heap dump. -// If your system doesn't support dumping, comment this out and install with -// `npm install --production` -// See: https://strongloop.github.io/strongloop.com/strongblog/how-to-heap-snapshots/ +/* globals module */ -// to enable this feature, uncomment the line below: -// require('heapdump'); +/* DISCLAIMER: -// we prepend a space because every usage expects it -// requiring admins to preserve it is unnecessarily confusing -var domain = ' ' + _domain; + There are two recommended methods of running a CryptPad instance: -// Content-Security-Policy -var baseCSP = [ - "default-src 'none'", - "style-src 'unsafe-inline' 'self' " + domain, - "font-src 'self' data:" + domain, + 1. Using a standalone nodejs server without HTTPS (suitable for local development) + 2. Using NGINX to serve static assets and to handle HTTPS for API server's websocket traffic - /* child-src is used to restrict iframes to a set of allowed domains. - * connect-src is used to restrict what domains can connect to the websocket. - * - * it is recommended that you configure these fields to match the - * domain which will serve your CryptPad instance. - */ - "child-src blob: *", - // IE/Edge - "frame-src blob: *", + We do not officially recommend or support Apache, Docker, Kubernetes, Traefik, or any other configuration. + Support requests for such setups should be directed to their authors. - /* 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, + If you're having difficulty difficulty configuring your instance + we suggest that you join the project's IRC/Matrix channel. - // data: is used by codemirror - "img-src 'self' data: blob:" + domain, - "media-src * blob:", + If you don't have any difficulty configuring your instance and you'd like to + support us for the work that went into making it pain-free we are quite happy + to accept donations via our opencollective page: https://opencollective.com/cryptpad - // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox - "frame-ancestors *", - "" -]; +*/ +module.exports = { +/* CryptPad is designed to serve its content over two domains. + * Account passwords and cryptographic content is handled on the 'main' domain, + * while the user interface is loaded on a 'sandbox' domain + * which can only access information which the main domain willingly shares. + * + * In the event of an XSS vulnerability in the UI (that's bad) + * this system prevents attackers from gaining access to your account (that's good). + * + * Most problems with new instances are related to this system blocking access + * because of incorrectly configured sandboxes. If you only see a white screen + * when you try to load CryptPad, this is probably the cause. + * + * PLEASE READ THE FOLLOWING COMMENTS CAREFULLY. + * + */ + +/* httpUnsafeOrigin is the URL that clients will enter to load your instance. + * Any other URL that somehow points to your instance is supposed to be blocked. + * The default provided below assumes you are loading CryptPad from a server + * which is running on the same machine, using port 3000. + * + * In a production instance this should be available ONLY over HTTPS + * using the default port for HTTPS (443) ie. https://cryptpad.fr + * In such a case this should be handled by NGINX, as documented in + * cryptpad/docs/example.nginx.conf (see the $main_domain variable) + * + */ + httpUnsafeOrigin: 'http://localhost:3000/', + +/* httpSafeOrigin is the URL that is used for the 'sandbox' described above. + * If you're testing or developing with CryptPad on your local machine then + * it is appropriate to leave this blank. The default behaviour is to serve + * the main domain over port 3000 and to serve the content over port 3001. + * + * This is not appropriate in a production environment where invasive networks + * may filter traffic going over abnormal ports. + * To correctly configure your production instance you must provide a URL + * with a different domain (a subdomain is sufficient). + * It will be used to load the UI in our 'sandbox' system. + * + * This value corresponds to the $sandbox_domain variable + * in the example nginx file. + * + * CUSTOMIZE AND UNCOMMENT THIS FOR PRODUCTION INSTALLATIONS. + */ + // httpSafeOrigin: "https://some-other-domain.xyz", +/* httpAddress specifies the address on which the nodejs server + * should be accessible. By default it will listen on 127.0.0.1 + * (IPv4 localhost on most systems). If you want it to listen on + * all addresses, including IPv6, set this to '::'. + * + */ + //httpAddress: '::', + +/* httpPort specifies on which port the nodejs server should listen. + * By default it will serve content over port 3000, which is suitable + * for both local development and for use with the provided nginx example, + * which will proxy websocket traffic to your node server. + * + */ + //httpPort: 3000, + +/* httpSafePort allows you to specify an alternative port from which + * the node process should serve sandboxed assets. The default value is + * that of your httpPort + 1. You probably don't need to change this. + * + */ + //httpSafePort: 3001, -module.exports = { /* ===================== * Admin * ===================== */ /* - * CryptPad now contains an administration panel. Its access is restricted to specific + * CryptPad contains an administration panel. Its access is restricted to specific * users using the following list. * To give access to the admin panel to a user account, just add their user id, * which can be found on the settings page for registered users. * Entries should be strings separated by a comma. */ +/* adminKeys: [ //"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=", ], +*/ /* CryptPad's administration panel includes a "support" tab * wherein administrators with a secret key can view messages @@ -76,159 +119,55 @@ module.exports = { */ // supportMailboxPublicKey: "", - /* ===================== - * Infra setup - * ===================== */ - - // the address you want to bind to, :: means all ipv4 and ipv6 addresses - // this may not work on all operating systems - httpAddress: '::', - - // the port on which your httpd will listen - httpPort: 3000, - - // This is for allowing the cross-domain iframe to function when developing - httpSafePort: 3001, - - // This is for deployment in production, CryptPad uses a separate origin (domain) to host the - // cross-domain iframe. It can simply host the same content as CryptPad. - // httpSafeOrigin: "https://some-other-domain.xyz", - - httpUnsafeOrigin: domain, - - /* Your CryptPad server will share this value with clients - * via its /api/config endpoint. - * - * If you want to host your API and asset servers on different hosts - * specify a URL for your API server websocket endpoint, like so: - * wss://api.yourdomain.com/cryptpad_websocket - * - * Otherwise, leave this commented and your clients will use the default - * websocket (wss://yourdomain.com/cryptpad_websocket) - */ - //externalWebsocketURL: 'wss://api.yourdomain.com/cryptpad_websocket - - /* CryptPad can be configured to send customized HTTP Headers - * These settings may vary widely depending on your needs - * Examples are provided below - */ - httpHeaders: { - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "Access-Control-Allow-Origin": "*" - }, - - contentSecurity: baseCSP.join('; ') + - "script-src 'self'" + domain, - - // CKEditor and OnlyOffice require significantly more lax content security policy in order to function. - padContentSecurity: baseCSP.join('; ') + - "script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain, - - /* Main pages - * add exceptions to the router so that we can access /privacy.html - * and other odd pages - */ - mainPages: [ - 'index', - 'privacy', - 'terms', - 'about', - 'contact', - 'what-is-cryptpad', - 'features', - 'faq', - 'maintenance' - ], - - /* ===================== - * Subscriptions - * ===================== */ - - /* Limits, Donations, Subscriptions and Contact + /* We're very proud that CryptPad is available to the public as free software! + * We do, however, still need to pay our bills as we develop the platform. * - * By default, CryptPad limits every registered user to 50MB of storage. It also shows a - * subscribe button which allows them to upgrade to a paid account. We handle payment, - * and keep 50% of the proceeds to fund ongoing development. + * By default CryptPad will prompt users to consider donating to + * our OpenCollective campaign. We publish the state of our finances periodically + * so you can decide for yourself whether our expenses are reasonable. * - * You can: - * A: leave things as they are - * B: disable accounts but display a donate button - * C: hide any reference to paid accounts or donation - * - * If you chose A then there's nothing to do. - * If you chose B, set 'allowSubscriptions' to false. - * If you chose C, set 'removeDonateButton' to true + * You can disable any solicitations for donations by setting 'removeDonateButton' to true, + * but we'd appreciate it if you didn't! */ - allowSubscriptions: true, - removeDonateButton: false, + //removeDonateButton: false, - /* - * By default, CryptPad also contacts our accounts server once a day to check for changes in - * the people who have accounts. This check-in will also send the version of your CryptPad - * instance and your email so we can reach you if we are aware of a serious problem. We will - * never sell it or send you marketing mail. If you want to block this check-in and remain - * completely invisible, set this and allowSubscriptions both to false. + /* CryptPad will display a point of contact for your instance on its contact page + * (/contact.html) if you provide it below. */ adminEmail: 'i.did.not.read.my.config@cryptpad.fr', - /* Sales coming from your server will be identified by your domain - * - * If you are using CryptPad in a business context, please consider taking a support contract - * by contacting sales@cryptpad.fr - */ - myDomain: _domain, - /* - * If you are using CryptPad internally and you want to increase the per-user storage limit, - * change the following value. + * By default, CryptPad contacts one of our servers once a day. + * This check-in will also send some very basic information about your instance including its + * version and the adminEmail so we can reach you if we are aware of a serious problem. + * We will never sell it or send you marketing mail. * - * Please note: This limit is what makes people subscribe and what pays for CryptPad - * development. Running a public instance that provides a "better deal" than cryptpad.fr - * is effectively using the project against itself. + * If you want to block this check-in and remain set 'blockDailyCheck' to true. */ - defaultStorageLimit: 50 * 1024 * 1024, + //blockDailyCheck: false, /* - * CryptPad allows administrators to give custom limits to their friends. - * add an entry for each friend, identified by their user id, - * which can be found on the settings page. Include a 'limit' (number of bytes), - * a 'plan' (string), and a 'note' (string). + * By default users get 50MB of storage by registering on an instance. + * You can set this value to whatever you want. * - * hint: 1GB is 1024 * 1024 * 1024 bytes + * hint: 50MB is 50 * 1024 * 1024 */ - customLimits: { - /* - "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { - limit: 20 * 1024 * 1024 * 1024, - plan: 'insider', - note: 'storage space donated by my.awesome.website' - }, - "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": { - limit: 10 * 1024 * 1024 * 1024, - plan: 'insider', - note: 'storage space donated by my.awesome.website' - } - */ - }, + //defaultStorageLimit: 50 * 1024 * 1024, + /* ===================== * STORAGE * ===================== */ - /* By default the CryptPad server will run scheduled tasks every five minutes - * If you want to run scheduled tasks in a separate process (like a crontab) - * you can disable this behaviour by setting the following value to true - */ - disableIntegratedTasks: false, - /* Pads that are not 'pinned' by any registered user can be set to expire * after a configurable number of days of inactivity (default 90 days). * The value can be changed or set to false to remove expiration. * Expired pads can then be removed using a cron job calling the - * `delete-inactive.js` script with node + * `evict-inactive.js` script with node + * + * defaults to 90 days if nothing is provided */ - inactiveTime: 90, // days + //inactiveTime: 90, // days /* CryptPad archives some data instead of deleting it outright. * This archived data still takes up space and so you'll probably still want to @@ -241,31 +180,46 @@ module.exports = { * deletion. Set this value to the number of days you'd like to retain * archived data before it's removed permanently. * + * defaults to 15 days if nothing is provided */ - archiveRetentionTime: 15, + //archiveRetentionTime: 15, /* Max Upload Size (bytes) * this sets the maximum size of any one file uploaded to the server. * anything larger than this size will be rejected + * defaults to 20MB if no value is provided */ - maxUploadSize: 20 * 1024 * 1024, - - // XXX - premiumUploadSize: 100 * 1024 * 1024, - - /* ===================== - * HARDWARE RELATED - * ===================== */ + //maxUploadSize: 20 * 1024 * 1024, - /* CryptPad's file storage adaptor closes unused files after a configurable - * number of milliseconds (default 30000 (30 seconds)) + /* + * CryptPad allows administrators to give custom limits to their friends. + * add an entry for each friend, identified by their user id, + * which can be found on the settings page. Include a 'limit' (number of bytes), + * a 'plan' (string), and a 'note' (string). + * + * hint: 1GB is 1024 * 1024 * 1024 bytes */ - channelExpirationMs: 30000, +/* + customLimits: { + "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { + limit: 20 * 1024 * 1024 * 1024, + plan: 'insider', + note: 'storage space donated by my.awesome.website' + }, + "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": { + limit: 10 * 1024 * 1024 * 1024, + plan: 'insider', + note: 'storage space donated by my.awesome.website' + } + }, +*/ - /* CryptPad's file storage adaptor is limited by the number of open files. - * When the adaptor reaches openFileLimit, it will clean up older files + /* Users with premium accounts (those with a plan included in their customLimit) + * can benefit from an increased upload size limit. By default they are restricted to the same + * upload size as any other registered user. + * */ - openFileLimit: 2048, + //premiumUploadSize: 100 * 1024 * 1024, /* ===================== * DATABASE VOLUMES diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 3c4b2b09e..117ddac87 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -107,7 +107,7 @@ define([ ])*/ ]) ]), - h('div.cp-version-footer', "CryptPad v3.12.0 (Megaloceros)") + h('div.cp-version-footer', "CryptPad v3.13.0 (NorthernWhiteRhino)") ]); }; diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 2960c1c3b..17948614b 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -431,7 +431,7 @@ width: 50px; margin: 0; min-width: 0; - font-size: 18px; + font-size: 18px !important; } } } diff --git a/lib/commands/channel.js b/lib/commands/channel.js index da4fb6685..c24837df3 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -5,6 +5,7 @@ const Util = require("../common-util"); const nThen = require("nthen"); const Core = require("./core"); const Metadata = require("./metadata"); +const HK = require("../hk-util"); Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) { if (typeof(channelId) !== 'string' || channelId.length !== 32) { @@ -228,7 +229,9 @@ Channel.isNewChannel = function (Env, channel, cb) { Otherwise behaves the same as sending to a channel */ -Channel.writePrivateMessage = function (Env, args, cb, Server) { +Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) { + var cb = Util.once(Util.mkAsync(_cb)); + var channelId = args[0]; var msg = args[1]; @@ -246,31 +249,52 @@ Channel.writePrivateMessage = function (Env, args, cb, Server) { return void cb("NOT_IMPLEMENTED"); } - // historyKeeper expects something with an 'id' attribute - // it will fail unless you provide it, but it doesn't need anything else - var channelStruct = { - id: channelId, - }; - - // construct a message to store and broadcast - var fullMessage = [ - 0, // idk - null, // normally the netflux id, null isn't rejected, and it distinguishes messages written in this way - "MSG", // indicate that this is a MSG - channelId, // channel id - msg // the actual message content. Generally a string - ]; - - // XXX RESTRICT respect allow lists - - // historyKeeper already knows how to handle metadata and message validation, so we just pass it off here - // if the message isn't valid it won't be stored. - Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage); - - Server.getChannelUserList(channelId).forEach(function (userId) { - Server.send(userId, fullMessage); - }); + nThen(function (w) { + Metadata.getMetadataRaw(Env, channelId, w(function (err, metadata) { + if (err) { + w.abort(); + Env.Log.error('HK_WRITE_PRIVATE_MESSAGE', err); + return void cb('METADATA_ERR'); + } + + if (!metadata || !metadata.restricted) { + return; + } + + var session = HK.getNetfluxSession(Env, netfluxId); + var allowed = HK.listAllowedUsers(metadata); + + if (HK.isUserSessionAllowed(allowed, session)) { return; } - cb(); + w.abort(); + cb('INSUFFICIENT_PERMISSIONS'); + })); + }).nThen(function () { + // historyKeeper expects something with an 'id' attribute + // it will fail unless you provide it, but it doesn't need anything else + var channelStruct = { + id: channelId, + }; + + // construct a message to store and broadcast + var fullMessage = [ + 0, // idk + null, // normally the netflux id, null isn't rejected, and it distinguishes messages written in this way + "MSG", // indicate that this is a MSG + channelId, // channel id + msg // the actual message content. Generally a string + ]; + + + // historyKeeper already knows how to handle metadata and message validation, so we just pass it off here + // if the message isn't valid it won't be stored. + Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage); + + Server.getChannelUserList(channelId).forEach(function (userId) { + Server.send(userId, fullMessage); + }); + + cb(); + }); }; diff --git a/lib/commands/quota.js b/lib/commands/quota.js index 74c4eca44..72d04cc68 100644 --- a/lib/commands/quota.js +++ b/lib/commands/quota.js @@ -38,6 +38,7 @@ Quota.updateCachedLimits = function (Env, cb) { if (Env.adminEmail === false) { Quota.applyCustomLimits(Env); if (Env.allowSubscriptions === false) { return; } + if (Env.blockDailyCheck === true) { return; } throw new Error("allowSubscriptions must be false if adminEmail is false"); } diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100644 index 000000000..6d8ec5e04 --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,86 @@ +var Default = module.exports; + +Default.commonCSP = function (domain) { + domain = ' ' + domain; + // Content-Security-Policy + + return [ + "default-src 'none'", + "style-src 'unsafe-inline' 'self' " + domain, + "font-src 'self' data:" + domain, + + /* child-src is used to restrict iframes to a set of allowed domains. + * connect-src is used to restrict what domains can connect to the websocket. + * + * it is recommended that you configure these fields to match the + * domain which will serve your CryptPad instance. + */ + "child-src blob: *", + // IE/Edge + "frame-src blob: *", + + /* 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, + + // data: is used by codemirror + "img-src 'self' data: blob:" + domain, + "media-src * blob:", + + // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox + "frame-ancestors *", + "" + ]; +}; + +Default.contentSecurity = function (domain) { + return (Default.commonCSP(domain).join('; ') + "script-src 'self' " + domain).replace(/\s+/g, ' '); +}; + +Default.padContentSecurity = function (domain) { + return (Default.commonCSP(domain).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' " + domain).replace(/\s+/g, ' '); +}; + +Default.httpHeaders = function () { + return { + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "Access-Control-Allow-Origin": "*" + }; +}; + +Default.mainPages = function () { + return [ + 'index', + 'privacy', + 'terms', + 'about', + 'contact', + 'what-is-cryptpad', + 'features', + 'faq', + 'maintenance' + ]; +}; + +/* By default the CryptPad server will run scheduled tasks every five minutes + * If you want to run scheduled tasks in a separate process (like a crontab) + * you can disable this behaviour by setting the following value to true + */ + //disableIntegratedTasks: false, + + /* CryptPad's file storage adaptor closes unused files after a configurable + * number of milliseconds (default 30000 (30 seconds)) + */ +// channelExpirationMs: 30000, + + /* CryptPad's file storage adaptor is limited by the number of open files. + * When the adaptor reaches openFileLimit, it will clean up older files + */ + //openFileLimit: 2048, + + + + diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index ed67602bd..8d2806550 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -65,10 +65,12 @@ module.exports.create = function (config, cb) { WARN: WARN, flushCache: config.flushCache, adminEmail: config.adminEmail, - allowSubscriptions: config.allowSubscriptions, - myDomain: config.myDomain, - mySubdomain: config.mySubdomain, - customLimits: config.customLimits, + allowSubscriptions: config.allowSubscriptions === true, + blockDailyCheck: config.blockDailyCheck === true, + + myDomain: config.httpUnsafeOrigin, + mySubdomain: config.mySubdomain, // only exists for the accounts integration + customLimits: config.customLimits || {}, // FIXME this attribute isn't in the default conf // but it is referenced in Quota domain: config.domain diff --git a/lib/hk-util.js b/lib/hk-util.js index 41d305172..810077c1c 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -834,6 +834,7 @@ const directMessageCommands = { */ HK.onDirectMessage = function (Env, Server, seq, userId, json) { const Log = Env.Log; + const HISTORY_KEEPER_ID = Env.id; Log.silly('HK_MESSAGE', json); let parsed; @@ -891,10 +892,27 @@ HK.onDirectMessage = function (Env, Server, seq, userId, json) { return; } - // XXX NOT ALLOWED - // respond to txid with error as in handleGetHistory - // send the allow list anyway, it might not get used currently - // but will in the future +/* Anyone in the userlist that isn't in the allow list should have already + been kicked out of the channel. Likewise, disallowed users should not + be able to add themselves to the userlist because JOIN commands respect + access control settings. The error that is sent below protects against + the remaining case, in which users try to get history without having + joined the channel. Normally we'd send the allow list to tell them the + key with which they should authenticate, but since we don't use this + behaviour, I'm doing the easy thing and just telling them to GO AWAY. + + We can implement the more advanced behaviour later if it turns out that + we need it. This command validates guards against all kinds of history + access: GET_HISTORY, GET_HISTORY_RANGE, GET_FULL_HISTORY. +*/ + + w.abort(); + return void Server.send(userId, [ + seq, + 'ERROR', + 'ERESTRICTED', + HISTORY_KEEPER_ID + ]); })); }).nThen(function () { // run the appropriate command from the map diff --git a/lib/load-config.js b/lib/load-config.js index 0756c2df4..4d6fa894f 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -1,7 +1,7 @@ /* jslint node: true */ "use strict"; var config; -var configPath = process.env.CRYPTPAD_CONFIG || "../config/config"; +var configPath = process.env.CRYPTPAD_CONFIG || "../config/config.js"; try { config = require(configPath); if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') { @@ -18,5 +18,29 @@ try { } config = require("../config/config.example"); } + +var isPositiveNumber = function (n) { + return (!isNaN(n) && n >= 0); +}; + +if (!isPositiveNumber(config.inactiveTime)) { + config.inactiveTime = 90; +} +if (!isPositiveNumber(config.archiveRetentionTime)) { + config.archiveRetentionTime = 90; +} +if (!isPositiveNumber(config.maxUploadSize)) { + config.maxUploadSize = 20 * 1024 * 1024; +} +if (!isPositiveNumber(config.defaultStorageLimit)) { + config.defaultStorageLimit = 50 * 1024 * 1024; +} + +// premiumUploadSize is worthless if it isn't a valid positive number +// or if it's less than the default upload size +if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < config.defaultStorageLimit) { + delete config.premiumUploadSize; +} + module.exports = config; diff --git a/package-lock.json b/package-lock.json index 770871394..d3ea0cef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.12.0", + "version": "3.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -114,6 +114,8 @@ }, "chainpad-server": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-4.0.3.tgz", + "integrity": "sha512-lTYd5Nk8iCm/2nrA6lQIbxEhw4OraBMxa6+7YQXZZlMI290Cpzna41hs63aXbwT+UR9llS5C9U6yLqVrlm7MCQ==", "requires": { "nthen": "0.1.8", "pull-stream": "^3.6.9", diff --git a/package.json b/package.json index 418637ffd..937d1883c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "3.12.0", + "version": "3.13.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/scripts/check-accounts.js b/scripts/check-accounts.js index 4d0067d43..9e75da0c6 100644 --- a/scripts/check-accounts.js +++ b/scripts/check-accounts.js @@ -4,7 +4,7 @@ var Config = require("../lib/load-config"); var Package = require("../package.json"); var body = JSON.stringify({ - domain: Config.myDomain, + domain: Config.myDomain || Config.httpUnsafeOrigin, subdomain: Config.mySubdomain || null, adminEmail: Config.adminEmail, version: Package.version, diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index e944a498b..56d2ccb3b 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -373,11 +373,24 @@ nThen(function (w) { } })); }).nThen(function (w) { - // XXX RESTRICT GET_METADATA should fail because alice is not on the allow list - // expect INSUFFICIENT_PERMISSIONS - alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err) { - if (!err) { - // XXX RESTRICT alice should not be permitted to read oscar's mailbox's metadata + alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err, response) { + if (!response) { throw new Error("EXPECTED RESPONSE"); } + var metadata = response[0]; + var expected_fields = ['restricted', 'allowed']; + for (var key in metadata) { + if (expected_fields.indexOf(key) === -1) { + console.log(metadata); + throw new Error("EXPECTED METADATA TO BE RESTRICTED"); + } + } + })); +}).nThen(function (w) { + alice.anonRpc.send('WRITE_PRIVATE_MESSAGE', [ + oscar.mailboxChannel, + '["VANDALISM"]', + ], w(function (err) { + if (err !== 'INSUFFICIENT_PERMISSIONS') { + throw new Error("EXPECTED INSUFFICIENT PERMISSIONS ERROR"); } })); }).nThen(function (w) { @@ -388,11 +401,17 @@ nThen(function (w) { value: [ alice.edKeys.edPublic ] - }, w(function (err /*, metadata */) { + }, w(function (err, response) { if (err) { + throw new Error("FAIL"); return void console.error(err); } - //console.log('XXX', metadata); + + var metadata = response && response[0]; + if (!metadata || !Array.isArray(metadata.allowed) || + metadata.allowed.indexOf(alice.edKeys.edPublic) === -1) { + throw new Error("EXPECTED ALICE TO BE IN THE ALLOW LIST"); + } })); }).nThen(function (w) { oscar.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err, response) { @@ -410,14 +429,12 @@ nThen(function (w) { } })); }).nThen(function () { - // XXX RESTRICT alice should now be able to read oscar's mailbox metadata -/* alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, function (err, response) { - if (err) { - PROBLEM + var metadata = response && response[0]; + if (!metadata || !metadata.restricted || !metadata.channel) { + throw new Error("EXPECTED FULL ACCESS TO CHANNEL METADATA"); } }); -*/ }).nThen(function (w) { //throw new Error("boop"); // add alice as an owner of oscar's mailbox for some reason diff --git a/server.js b/server.js index ddf9fc8b0..7b0e93687 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,7 @@ var Package = require('./package.json'); var Path = require("path"); var nThen = require("nthen"); var Util = require("./lib/common-util"); +var Default = require("./lib/defaults"); var config = require("./lib/load-config"); @@ -35,6 +36,47 @@ if (process.env.PACKAGE) { FRESH_KEY = +new Date(); } +(function () { + // you absolutely must provide an 'httpUnsafeOrigin' + if (typeof(config.httpUnsafeOrigin) !== 'string') { + throw new Error("No 'httpUnsafeOrigin' provided"); + } + + config.httpUnsafeOrigin = config.httpUnsafeOrigin.trim(); + + // 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(httpSafeOrigin) !== 'string') { + if (typeof(config.httpSafePort) !== 'number') { + config.httpSafePort = config.httpPort + 1; + } + + if (DEV_MODE) { return; } + console.log(` + m m mm mmmmm mm m mmmmm mm m mmm m + # # # ## # "# #"m # # #"m # m" " # + " #"# # # # #mmmm" # #m # # # #m # # mm # + ## ##" #mm# # "m # # # # # # # # # + # # # # # " # ## mm#mm # ## "mmm" # +`); + + console.log("\nNo 'httpSafeOrigin' provided."); + console.log("Your configuration probably isn't taking advantage of all of CryptPad's security features!"); + console.log("This is acceptable for development, otherwise your users may be at risk.\n"); + + console.log("Serving sandboxed content via port %s.\nThis is probably not what you want for a production instance!\n", config.httpSafePort); + } +}()); + var configCache = {}; config.flushCache = function () { configCache = {}; @@ -47,11 +89,21 @@ config.flushCache = function () { const clone = (x) => (JSON.parse(JSON.stringify(x))); var setHeaders = (function () { - if (typeof(config.httpHeaders) !== 'object') { return function () {}; } + // load the default http headers unless the admin has provided their own via the config file + var headers; + + var custom = config.httpHeaders; + // if the admin provided valid http headers then use them + if (custom && typeof(custom) === 'object' && !Array.isArray(custom)) { + headers = clone(custom); + } else { + // otherwise use the default + headers = Default.httpHeaders(); + } - const headers = clone(config.httpHeaders); - if (config.contentSecurity) { - headers['Content-Security-Policy'] = clone(config.contentSecurity); + // next define the base Content Security Policy (CSP) headers + if (typeof(config.contentSecurity) === 'string') { + 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 @@ -59,10 +111,16 @@ var setHeaders = (function () { // 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(config.httpUnsafeOrigin); } + const padHeaders = clone(headers); - if (config.padContentSecurity) { - padHeaders['Content-Security-Policy'] = clone(config.padContentSecurity); + if (typeof(config.padContentSecurity) === 'string') { + padHeaders['Content-Security-Policy'] = config.padContentSecurity; + } else { + padHeaders['Content-Security-Policy'] = Default.padContentSecurity(config.httpUnsafeOrigin); } if (Object.keys(headers).length) { return function (req, res) { @@ -116,7 +174,7 @@ app.use(Express.static(__dirname + '/www')); // FIXME I think this is a regression caused by a recent PR // correct this hack without breaking the contributor's intended behaviour. -var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'contact']; +var mainPages = config.mainPages || Default.mainPages(); var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); app.get(mainPagePattern, Express.static(__dirname + '/customize')); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); @@ -163,11 +221,13 @@ var serveConfig = (function () { removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), websocketPath: config.externalWebsocketURL, - httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), + httpUnsafeOrigin: config.httpUnsafeOrigin, adminEmail: config.adminEmail, adminKeys: admins, inactiveTime: config.inactiveTime, - supportMailbox: config.supportMailboxPublicKey + supportMailbox: config.supportMailboxPublicKey, + maxUploadSize: config.maxUploadSize, + premiumUploadSize: config.premiumUploadSize, }, null, '\t'), 'obj.httpSafeOrigin = ' + (function () { if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 4f6e73ba0..b3d285875 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -953,7 +953,6 @@ define([ 'data-curve': data.curvePublic || '', 'data-name': name.toLowerCase(), 'data-order': i, - title: name, style: 'order:'+i+';' },[ avatar, @@ -2426,9 +2425,9 @@ define([ case 'access': button = $('