From aac7c380c8c389b0683b2bd64e2cc856993f0e30 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sun, 1 Mar 2020 20:59:53 +0200 Subject: - Fix critical and moderate security vulnerabilities reported at 2020-02-26 with responsible disclosure by [Dejan Zelic](https://twitter.com/dejandayoff), Justin Benjamin and others at [Offensive Security](https://twitter.com/offsectraining), that follow standard 90 days before public disclosure. Thanks to xet7. - Fix webhook error that prevented some card etc deleting from web UI of board. Thanks to xet7. - Add some more Font Awesome icons. Thanks to xet7. - Remove autofocus from many form input boxes so that they would not cause warnings. Thanks to xet7. --- server/notifications/outgoing.js | 361 ++++++++++++++++++++------------------- server/statistics.js | 138 ++++++++------- 2 files changed, 257 insertions(+), 242 deletions(-) (limited to 'server') diff --git a/server/notifications/outgoing.js b/server/notifications/outgoing.js index 5bc2c540..9a741ea1 100644 --- a/server/notifications/outgoing.js +++ b/server/notifications/outgoing.js @@ -1,192 +1,199 @@ -const postCatchError = Meteor.wrapAsync((url, options, resolve) => { - HTTP.post(url, options, (err, res) => { - if (err) { - resolve(null, err.response); - } else { - resolve(null, res); - } +if (Meteor.isServer) { + const postCatchError = Meteor.wrapAsync((url, options, resolve) => { + HTTP.post(url, options, (err, res) => { + if (err) { + resolve(null, err.response); + } else { + resolve(null, res); + } + }); }); -}); -const Lock = { - _lock: {}, - _timer: {}, - echoDelay: 500, // echo should be happening much faster - normalDelay: 1e3, // normally user typed comment will be much slower - ECHO: 2, - NORMAL: 1, - NULL: 0, - has(id, value) { - const existing = this._lock[id]; - let ret = this.NULL; - if (existing) { - ret = existing === value ? this.ECHO : this.NORMAL; - } - return ret; - }, - clear(id, delay) { - const previous = this._timer[id]; - if (previous) { - Meteor.clearTimeout(previous); - } - this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay); - }, - set(id, value) { - const state = this.has(id, value); - let delay = this.normalDelay; - if (state === this.ECHO) { - delay = this.echoDelay; - } - if (!value) { - // user commented, we set a lock - value = 1; - } - this._lock[id] = value; - this.clear(id, delay); // always auto reset the locker after delay - }, - unset(id) { - delete this._lock[id]; - }, -}; + const Lock = { + _lock: {}, + _timer: {}, + echoDelay: 500, // echo should be happening much faster + normalDelay: 1e3, // normally user typed comment will be much slower + ECHO: 2, + NORMAL: 1, + NULL: 0, + has(id, value) { + const existing = this._lock[id]; + let ret = this.NULL; + if (existing) { + ret = existing === value ? this.ECHO : this.NORMAL; + } + return ret; + }, + clear(id, delay) { + const previous = this._timer[id]; + if (previous) { + Meteor.clearTimeout(previous); + } + this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay); + }, + set(id, value) { + const state = this.has(id, value); + let delay = this.normalDelay; + if (state === this.ECHO) { + delay = this.echoDelay; + } + if (!value) { + // user commented, we set a lock + value = 1; + } + this._lock[id] = value; + this.clear(id, delay); // always auto reset the locker after delay + }, + unset(id) { + delete this._lock[id]; + }, + }; -const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES && - process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [ - 'cardId', - 'listId', - 'oldListId', - 'boardId', - 'comment', - 'user', - 'card', - 'commentId', - 'swimlaneId', -]; -const responseFunc = data => { - const paramCommentId = data.commentId; - const paramCardId = data.cardId; - const paramBoardId = data.boardId; - const newComment = data.comment; - if (paramCardId && paramBoardId && newComment) { - // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data - const comment = CardComments.findOne({ - _id: paramCommentId, - cardId: paramCardId, - boardId: paramBoardId, - }); - const board = Boards.findOne(paramBoardId); - const card = Cards.findOne(paramCardId); - if (board && card) { - if (comment) { - Lock.set(comment._id, newComment); - CardComments.direct.update(comment._id, { - $set: { + const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES && + process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [ + 'cardId', + 'listId', + 'oldListId', + 'boardId', + 'comment', + 'user', + 'card', + 'commentId', + 'swimlaneId', + ]; + const responseFunc = data => { + const paramCommentId = data.commentId; + const paramCardId = data.cardId; + const paramBoardId = data.boardId; + const newComment = data.comment; + if (paramCardId && paramBoardId && newComment) { + // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data + const comment = CardComments.findOne({ + _id: paramCommentId, + cardId: paramCardId, + boardId: paramBoardId, + }); + const board = Boards.findOne(paramBoardId); + const card = Cards.findOne(paramCardId); + if (board && card) { + if (comment) { + Lock.set(comment._id, newComment); + CardComments.direct.update(comment._id, { + $set: { + text: newComment, + }, + }); + } + } else { + const userId = data.userId; + if (userId) { + const inserted = CardComments.direct.insert({ text: newComment, - }, - }); - } - } else { - const userId = data.userId; - if (userId) { - const inserted = CardComments.direct.insert({ - text: newComment, - userId, - cardId, - boardId, - }); - Lock.set(inserted._id, newComment); + userId, + cardId, + boardId, + }); + Lock.set(inserted._id, newComment); + } } } - } -}; -Meteor.methods({ - outgoingWebhooks(integration, description, params) { - check(integration, Object); - check(description, String); - check(params, Object); - this.unblock(); + }; + Meteor.methods({ + outgoingWebhooks(integration, description, params) { + if (Meteor.user()) { + check(integration, Object); + check(description, String); + check(params, Object); + this.unblock(); - // label activity did not work yet, see wekan/models/activities.js - const quoteParams = _.clone(params); - const clonedParams = _.clone(params); - [ - 'card', - 'list', - 'oldList', - 'board', - 'oldBoard', - 'comment', - 'checklist', - 'swimlane', - 'oldSwimlane', - 'label', - 'attachment', - ].forEach(key => { - if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`; - }); + // label activity did not work yet, see wekan/models/activities.js + const quoteParams = _.clone(params); + const clonedParams = _.clone(params); + [ + 'card', + 'list', + 'oldList', + 'board', + 'oldBoard', + 'comment', + 'checklist', + 'swimlane', + 'oldSwimlane', + 'label', + 'attachment', + ].forEach(key => { + if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`; + }); - const userId = params.userId ? params.userId : integrations[0].userId; - const user = Users.findOne(userId); - const text = `${params.user} ${TAPi18n.__( - description, - quoteParams, - user.getLanguage(), - )}\n${params.url}`; + const userId = params.userId ? params.userId : integrations[0].userId; + const user = Users.findOne(userId); + const text = `${params.user} ${TAPi18n.__( + description, + quoteParams, + user.getLanguage(), + )}\n${params.url}`; - if (text.length === 0) return; + if (text.length === 0) return; - const value = { - text: `${text}`, - }; + const value = { + text: `${text}`, + }; - webhooksAtbts.forEach(key => { - if (params[key]) value[key] = params[key]; - }); - value.description = description; - //integrations.forEach(integration => { - const is2way = integration.type === Integrations.Const.TWOWAY; - const token = integration.token || ''; - const headers = { - 'Content-Type': 'application/json', - }; - if (token) headers['X-Wekan-Token'] = token; - const options = { - headers, - data: is2way ? { description, ...clonedParams } : value, - }; - const url = integration.url; - if (is2way) { - const cid = params.commentId; - const comment = params.comment; - const lockState = cid && Lock.has(cid, comment); - if (cid && lockState !== Lock.NULL) { - // it's a comment and there is a previous lock - return; - } else if (cid) { - Lock.set(cid, comment); // set a lock here - } - } - const response = postCatchError(url, options); + webhooksAtbts.forEach(key => { + if (params[key]) value[key] = params[key]; + }); + value.description = description; + //integrations.forEach(integration => { + const is2way = integration.type === Integrations.Const.TWOWAY; + const token = integration.token || ''; + const headers = { + 'Content-Type': 'application/json', + }; + if (token) headers['X-Wekan-Token'] = token; + const options = { + headers, + data: is2way ? { description, ...clonedParams } : value, + }; - if ( - response && - response.statusCode && - response.statusCode >= 200 && - response.statusCode < 300 - ) { - if (is2way) { - const data = response.data; // only an JSON encoded response will be actioned - if (data) { - try { - responseFunc(data); - } catch (e) { - throw new Meteor.Error('error-process-data'); + if (!Integrations.findOne({ url: integration.url })) return; + + const url = integration.url; + + if (is2way) { + const cid = params.commentId; + const comment = params.comment; + const lockState = cid && Lock.has(cid, comment); + if (cid && lockState !== Lock.NULL) { + // it's a comment and there is a previous lock + return; + } else if (cid) { + Lock.set(cid, comment); // set a lock here } } + const response = postCatchError(url, options); + + if ( + response && + response.statusCode && + response.statusCode >= 200 && + response.statusCode < 300 + ) { + if (is2way) { + const data = response.data; // only an JSON encoded response will be actioned + if (data) { + try { + responseFunc(data); + } catch (e) { + throw new Meteor.Error('error-process-data'); + } + } + } + return response; // eslint-disable-line consistent-return + } else { + throw new Meteor.Error('error-invalid-webhook-response'); + } } - return response; // eslint-disable-line consistent-return - } else { - throw new Meteor.Error('error-invalid-webhook-response'); - } - //}); - }, -}); + }, + }); +} diff --git a/server/statistics.js b/server/statistics.js index 997fd86f..0ead840f 100644 --- a/server/statistics.js +++ b/server/statistics.js @@ -1,68 +1,76 @@ import { MongoInternals } from 'meteor/mongo'; -Meteor.methods({ - getStatistics() { - const os = require('os'); - const pjson = require('/package.json'); - const statistics = {}; - let wekanVersion = pjson.version; - wekanVersion = wekanVersion.replace('v', ''); - statistics.version = wekanVersion; - statistics.os = { - type: os.type(), - platform: os.platform(), - arch: os.arch(), - release: os.release(), - uptime: os.uptime(), - loadavg: os.loadavg(), - totalmem: os.totalmem(), - freemem: os.freemem(), - cpus: os.cpus(), - }; - let nodeVersion = process.version; - nodeVersion = nodeVersion.replace('v', ''); - statistics.process = { - nodeVersion, - pid: process.pid, - uptime: process.uptime(), - }; - // Remove beginning of Meteor release text METEOR@ - let meteorVersion = Meteor.release; - meteorVersion = meteorVersion.replace('METEOR@', ''); - statistics.meteor = { - meteorVersion, - }; - // Thanks to RocketChat for MongoDB version detection ! - // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js - let mongoVersion; - let mongoStorageEngine; - let mongoOplogEnabled; - try { - const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); - oplogEnabled = Boolean( - mongo._oplogHandle && mongo._oplogHandle.onOplogEntry, - ); - const { version, storageEngine } = Promise.await( - mongo.db.command({ serverStatus: 1 }), - ); - mongoVersion = version; - mongoStorageEngine = storageEngine.name; - mongoOplogEnabled = oplogEnabled; - } catch (e) { - try { - const { version } = Promise.await(mongo.db.command({ buildinfo: 1 })); - mongoVersion = version; - mongoStorageEngine = 'unknown'; - } catch (e) { - mongoVersion = 'unknown'; - mongoStorageEngine = 'unknown'; +if (Meteor.isServer) { + Meteor.methods({ + getStatistics() { + if (Meteor.user() && Meteor.user().isAdmin) { + const os = require('os'); + const pjson = require('/package.json'); + const statistics = {}; + let wekanVersion = pjson.version; + wekanVersion = wekanVersion.replace('v', ''); + statistics.version = wekanVersion; + statistics.os = { + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + uptime: os.uptime(), + loadavg: os.loadavg(), + totalmem: os.totalmem(), + freemem: os.freemem(), + cpus: os.cpus(), + }; + let nodeVersion = process.version; + nodeVersion = nodeVersion.replace('v', ''); + statistics.process = { + nodeVersion, + pid: process.pid, + uptime: process.uptime(), + }; + // Remove beginning of Meteor release text METEOR@ + let meteorVersion = Meteor.release; + meteorVersion = meteorVersion.replace('METEOR@', ''); + statistics.meteor = { + meteorVersion, + }; + // Thanks to RocketChat for MongoDB version detection ! + // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js + let mongoVersion; + let mongoStorageEngine; + let mongoOplogEnabled; + try { + const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); + oplogEnabled = Boolean( + mongo._oplogHandle && mongo._oplogHandle.onOplogEntry, + ); + const { version, storageEngine } = Promise.await( + mongo.db.command({ serverStatus: 1 }), + ); + mongoVersion = version; + mongoStorageEngine = storageEngine.name; + mongoOplogEnabled = oplogEnabled; + } catch (e) { + try { + const { version } = Promise.await( + mongo.db.command({ buildinfo: 1 }), + ); + mongoVersion = version; + mongoStorageEngine = 'unknown'; + } catch (e) { + mongoVersion = 'unknown'; + mongoStorageEngine = 'unknown'; + } + } + statistics.mongo = { + mongoVersion, + mongoStorageEngine, + mongoOplogEnabled, + }; + return statistics; + } else { + return false; } - } - statistics.mongo = { - mongoVersion, - mongoStorageEngine, - mongoOplogEnabled, - }; - return statistics; - }, -}); + }, + }); +} -- cgit v1.2.3-1-g7c22