- 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.
reviewable/pr2955/r1
Lauri Ojansivu 5 years ago
parent fc35c234a7
commit aac7c380c8
  1. 4
      client/components/settings/peopleBody.jade
  2. 9
      client/components/sidebar/sidebar.jade
  3. 9
      models/activities.js
  4. 169
      models/users.js
  5. 361
      server/notifications/outgoing.js
  6. 138
      server/statistics.js

@ -110,7 +110,7 @@ template(name="editUserPopup")
label.hide.userId(type="text" value=user._id) label.hide.userId(type="text" value=user._id)
label label
| {{_ 'fullname'}} | {{_ 'fullname'}}
input.js-profile-fullname(type="text" value=user.profile.fullname autofocus) input.js-profile-fullname(type="text" value=user.profile.fullname)
label label
| {{_ 'username'}} | {{_ 'username'}}
span.error.hide.username-taken span.error.hide.username-taken
@ -159,7 +159,7 @@ template(name="newUserPopup")
//label.hide.userId(type="text" value=user._id) //label.hide.userId(type="text" value=user._id)
label label
| {{_ 'fullname'}} | {{_ 'fullname'}}
input.js-profile-fullname(type="text" value="" autofocus) input.js-profile-fullname(type="text" value="")
label label
| {{_ 'username'}} | {{_ 'username'}}
span.error.hide.username-taken span.error.hide.username-taken

@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
b   b  
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}") .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title) input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus) input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token") input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
select.js-outgoing-webhooks-type(name="type") select.js-outgoing-webhooks-type(name="type")
each _type in types each _type in types
@ -257,7 +257,7 @@ template(name="outgoingWebhooksPopup")
input(type="hidden" value=_id name="id") input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}") input.primary.wide(type="submit" value="{{_ 'save'}}")
form.integration-form form.integration-form
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus) input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url") input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token") input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
select.js-outgoing-webhooks-type(name="type") select.js-outgoing-webhooks-type(name="type")
@ -267,7 +267,10 @@ template(name="outgoingWebhooksPopup")
template(name="boardMenuPopup") template(name="boardMenuPopup")
ul.pop-over-list ul.pop-over-list
li: a.js-custom-fields {{_ 'custom-fields'}} li
a.js-custom-fields
i.fa.fa-list-alt
| {{_ 'custom-fields'}}
li li
a.js-open-archives a.js-open-archives
i.fa.fa-archive i.fa.fa-archive

@ -108,7 +108,7 @@ if (Meteor.isServer) {
let participants = []; let participants = [];
let watchers = []; let watchers = [];
let title = 'act-activity-notify'; let title = 'act-activity-notify';
let board = null; const board = Boards.findOne(activity.boardId);
const description = `act-${activity.activityType}`; const description = `act-${activity.activityType}`;
const params = { const params = {
activityId: activity._id, activityId: activity._id,
@ -122,8 +122,11 @@ if (Meteor.isServer) {
params.userId = activity.userId; params.userId = activity.userId;
} }
if (activity.boardId) { if (activity.boardId) {
board = activity.board(); if (board.title.length > 0) {
params.board = board.title; params.board = board.title;
} else {
params.board = '';
}
title = 'act-withBoardTitle'; title = 'act-withBoardTitle';
params.url = board.absoluteUrl(); params.url = board.absoluteUrl();
params.boardId = activity.boardId; params.boardId = activity.boardId;

@ -620,44 +620,6 @@ Users.mutations({
}); });
Meteor.methods({ Meteor.methods({
setCreateUser(fullname, username, password, isAdmin, isActive, email) {
if (Meteor.user().isAdmin) {
check(fullname, String);
check(username, String);
check(password, String);
check(isAdmin, String);
check(isActive, String);
check(email, String);
const nUsersWithUsername = Users.find({ username }).count();
const nUsersWithEmail = Users.find({ email }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else if (nUsersWithEmail > 0) {
throw new Meteor.Error('email-already-taken');
} else {
Accounts.createUser({
fullname,
username,
password,
isAdmin,
isActive,
email: email.toLowerCase(),
from: 'admin',
});
}
}
},
setUsername(username, userId) {
check(username, String);
check(userId, String);
const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else {
Users.update(userId, { $set: { username } });
}
},
setListSortBy(value) { setListSortBy(value) {
check(value, String); check(value, String);
Meteor.user().setListSortBy(value); Meteor.user().setListSortBy(value);
@ -678,51 +640,97 @@ Meteor.methods({
check(limit, Number); check(limit, Number);
Meteor.user().setShowCardsCountAt(limit); Meteor.user().setShowCardsCountAt(limit);
}, },
setEmail(email, userId) {
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } },
);
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
emails: [
{
address: email,
verified: false,
},
],
},
});
}
},
setUsernameAndEmail(username, email, userId) {
check(username, String);
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
check(userId, String);
Meteor.call('setUsername', username, userId);
Meteor.call('setEmail', email, userId);
},
setPassword(newPassword, userId) {
check(userId, String);
check(newPassword, String);
if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
},
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.methods({ Meteor.methods({
setCreateUser(fullname, username, password, isAdmin, isActive, email) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(fullname, String);
check(username, String);
check(password, String);
check(isAdmin, String);
check(isActive, String);
check(email, String);
const nUsersWithUsername = Users.find({ username }).count();
const nUsersWithEmail = Users.find({ email }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else if (nUsersWithEmail > 0) {
throw new Meteor.Error('email-already-taken');
} else {
Accounts.createUser({
fullname,
username,
password,
isAdmin,
isActive,
email: email.toLowerCase(),
from: 'admin',
});
}
}
},
setUsername(username, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(username, String);
check(userId, String);
const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else {
Users.update(userId, { $set: { username } });
}
}
},
setEmail(email, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } },
);
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
emails: [
{
address: email,
verified: false,
},
],
},
});
}
}
},
setUsernameAndEmail(username, email, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(username, String);
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
check(userId, String);
Meteor.call('setUsername', username, userId);
Meteor.call('setEmail', email, userId);
}
},
setPassword(newPassword, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(userId, String);
check(newPassword, String);
if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
}
},
// we accept userId, username, email // we accept userId, username, email
inviteUserToBoard(username, boardId) { inviteUserToBoard(username, boardId) {
check(username, String); check(username, String);
@ -754,8 +762,9 @@ if (Meteor.isServer) {
throw new Meteor.Error('error-user-notAllowSelf'); throw new Meteor.Error('error-user-notAllowSelf');
} else { } else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist'); if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
if (Settings.findOne().disableRegistration) if (Settings.findOne({ disableRegistration: true })) {
throw new Meteor.Error('error-user-notCreated'); throw new Meteor.Error('error-user-notCreated');
}
// Set in lowercase email before creating account // Set in lowercase email before creating account
const email = username.toLowerCase(); const email = username.toLowerCase();
username = email.substring(0, posAt); username = email.substring(0, posAt);

@ -1,192 +1,199 @@
const postCatchError = Meteor.wrapAsync((url, options, resolve) => { if (Meteor.isServer) {
HTTP.post(url, options, (err, res) => { const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
if (err) { HTTP.post(url, options, (err, res) => {
resolve(null, err.response); if (err) {
} else { resolve(null, err.response);
resolve(null, res); } else {
} resolve(null, res);
}
});
}); });
});
const Lock = { const Lock = {
_lock: {}, _lock: {},
_timer: {}, _timer: {},
echoDelay: 500, // echo should be happening much faster echoDelay: 500, // echo should be happening much faster
normalDelay: 1e3, // normally user typed comment will be much slower normalDelay: 1e3, // normally user typed comment will be much slower
ECHO: 2, ECHO: 2,
NORMAL: 1, NORMAL: 1,
NULL: 0, NULL: 0,
has(id, value) { has(id, value) {
const existing = this._lock[id]; const existing = this._lock[id];
let ret = this.NULL; let ret = this.NULL;
if (existing) { if (existing) {
ret = existing === value ? this.ECHO : this.NORMAL; ret = existing === value ? this.ECHO : this.NORMAL;
} }
return ret; return ret;
}, },
clear(id, delay) { clear(id, delay) {
const previous = this._timer[id]; const previous = this._timer[id];
if (previous) { if (previous) {
Meteor.clearTimeout(previous); Meteor.clearTimeout(previous);
} }
this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay); this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
}, },
set(id, value) { set(id, value) {
const state = this.has(id, value); const state = this.has(id, value);
let delay = this.normalDelay; let delay = this.normalDelay;
if (state === this.ECHO) { if (state === this.ECHO) {
delay = this.echoDelay; delay = this.echoDelay;
} }
if (!value) { if (!value) {
// user commented, we set a lock // user commented, we set a lock
value = 1; value = 1;
} }
this._lock[id] = value; this._lock[id] = value;
this.clear(id, delay); // always auto reset the locker after delay this.clear(id, delay); // always auto reset the locker after delay
}, },
unset(id) { unset(id) {
delete this._lock[id]; delete this._lock[id];
}, },
}; };
const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES && const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [ process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
'cardId', 'cardId',
'listId', 'listId',
'oldListId', 'oldListId',
'boardId', 'boardId',
'comment', 'comment',
'user', 'user',
'card', 'card',
'commentId', 'commentId',
'swimlaneId', 'swimlaneId',
]; ];
const responseFunc = data => { const responseFunc = data => {
const paramCommentId = data.commentId; const paramCommentId = data.commentId;
const paramCardId = data.cardId; const paramCardId = data.cardId;
const paramBoardId = data.boardId; const paramBoardId = data.boardId;
const newComment = data.comment; const newComment = data.comment;
if (paramCardId && paramBoardId && newComment) { 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 // 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({ const comment = CardComments.findOne({
_id: paramCommentId, _id: paramCommentId,
cardId: paramCardId, cardId: paramCardId,
boardId: paramBoardId, boardId: paramBoardId,
}); });
const board = Boards.findOne(paramBoardId); const board = Boards.findOne(paramBoardId);
const card = Cards.findOne(paramCardId); const card = Cards.findOne(paramCardId);
if (board && card) { if (board && card) {
if (comment) { if (comment) {
Lock.set(comment._id, newComment); Lock.set(comment._id, newComment);
CardComments.direct.update(comment._id, { CardComments.direct.update(comment._id, {
$set: { $set: {
text: newComment,
},
});
}
} else {
const userId = data.userId;
if (userId) {
const inserted = CardComments.direct.insert({
text: newComment, text: newComment,
}, userId,
}); cardId,
} boardId,
} else { });
const userId = data.userId; Lock.set(inserted._id, newComment);
if (userId) { }
const inserted = CardComments.direct.insert({
text: newComment,
userId,
cardId,
boardId,
});
Lock.set(inserted._id, newComment);
} }
} }
} };
}; Meteor.methods({
Meteor.methods({ outgoingWebhooks(integration, description, params) {
outgoingWebhooks(integration, description, params) { if (Meteor.user()) {
check(integration, Object); check(integration, Object);
check(description, String); check(description, String);
check(params, Object); check(params, Object);
this.unblock(); this.unblock();
// label activity did not work yet, see wekan/models/activities.js // label activity did not work yet, see wekan/models/activities.js
const quoteParams = _.clone(params); const quoteParams = _.clone(params);
const clonedParams = _.clone(params); const clonedParams = _.clone(params);
[ [
'card', 'card',
'list', 'list',
'oldList', 'oldList',
'board', 'board',
'oldBoard', 'oldBoard',
'comment', 'comment',
'checklist', 'checklist',
'swimlane', 'swimlane',
'oldSwimlane', 'oldSwimlane',
'label', 'label',
'attachment', 'attachment',
].forEach(key => { ].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`; if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
}); });
const userId = params.userId ? params.userId : integrations[0].userId; const userId = params.userId ? params.userId : integrations[0].userId;
const user = Users.findOne(userId); const user = Users.findOne(userId);
const text = `${params.user} ${TAPi18n.__( const text = `${params.user} ${TAPi18n.__(
description, description,
quoteParams, quoteParams,
user.getLanguage(), user.getLanguage(),
)}\n${params.url}`; )}\n${params.url}`;
if (text.length === 0) return; if (text.length === 0) return;
const value = { const value = {
text: `${text}`, text: `${text}`,
}; };
webhooksAtbts.forEach(key => { webhooksAtbts.forEach(key => {
if (params[key]) value[key] = params[key]; if (params[key]) value[key] = params[key];
}); });
value.description = description; value.description = description;
//integrations.forEach(integration => { //integrations.forEach(integration => {
const is2way = integration.type === Integrations.Const.TWOWAY; const is2way = integration.type === Integrations.Const.TWOWAY;
const token = integration.token || ''; const token = integration.token || '';
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
if (token) headers['X-Wekan-Token'] = token; if (token) headers['X-Wekan-Token'] = token;
const options = { const options = {
headers, headers,
data: is2way ? { description, ...clonedParams } : value, 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);
if ( if (!Integrations.findOne({ url: integration.url })) return;
response &&
response.statusCode && const url = integration.url;
response.statusCode >= 200 &&
response.statusCode < 300 if (is2way) {
) { const cid = params.commentId;
if (is2way) { const comment = params.comment;
const data = response.data; // only an JSON encoded response will be actioned const lockState = cid && Lock.has(cid, comment);
if (data) { if (cid && lockState !== Lock.NULL) {
try { // it's a comment and there is a previous lock
responseFunc(data); return;
} catch (e) { } else if (cid) {
throw new Meteor.Error('error-process-data'); 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'); }
}
//});
},
});

@ -1,68 +1,76 @@
import { MongoInternals } from 'meteor/mongo'; import { MongoInternals } from 'meteor/mongo';
Meteor.methods({ if (Meteor.isServer) {
getStatistics() { Meteor.methods({
const os = require('os'); getStatistics() {
const pjson = require('/package.json'); if (Meteor.user() && Meteor.user().isAdmin) {
const statistics = {}; const os = require('os');
let wekanVersion = pjson.version; const pjson = require('/package.json');
wekanVersion = wekanVersion.replace('v', ''); const statistics = {};
statistics.version = wekanVersion; let wekanVersion = pjson.version;
statistics.os = { wekanVersion = wekanVersion.replace('v', '');
type: os.type(), statistics.version = wekanVersion;
platform: os.platform(), statistics.os = {
arch: os.arch(), type: os.type(),
release: os.release(), platform: os.platform(),
uptime: os.uptime(), arch: os.arch(),
loadavg: os.loadavg(), release: os.release(),
totalmem: os.totalmem(), uptime: os.uptime(),
freemem: os.freemem(), loadavg: os.loadavg(),
cpus: os.cpus(), totalmem: os.totalmem(),
}; freemem: os.freemem(),
let nodeVersion = process.version; cpus: os.cpus(),
nodeVersion = nodeVersion.replace('v', ''); };
statistics.process = { let nodeVersion = process.version;
nodeVersion, nodeVersion = nodeVersion.replace('v', '');
pid: process.pid, statistics.process = {
uptime: process.uptime(), nodeVersion,
}; pid: process.pid,
// Remove beginning of Meteor release text METEOR@ uptime: process.uptime(),
let meteorVersion = Meteor.release; };
meteorVersion = meteorVersion.replace('METEOR@', ''); // Remove beginning of Meteor release text METEOR@
statistics.meteor = { let meteorVersion = Meteor.release;
meteorVersion, meteorVersion = meteorVersion.replace('METEOR@', '');
}; statistics.meteor = {
// Thanks to RocketChat for MongoDB version detection ! meteorVersion,
// https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js };
let mongoVersion; // Thanks to RocketChat for MongoDB version detection !
let mongoStorageEngine; // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js
let mongoOplogEnabled; let mongoVersion;
try { let mongoStorageEngine;
const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); let mongoOplogEnabled;
oplogEnabled = Boolean( try {
mongo._oplogHandle && mongo._oplogHandle.onOplogEntry, const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
); oplogEnabled = Boolean(
const { version, storageEngine } = Promise.await( mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
mongo.db.command({ serverStatus: 1 }), );
); const { version, storageEngine } = Promise.await(
mongoVersion = version; mongo.db.command({ serverStatus: 1 }),
mongoStorageEngine = storageEngine.name; );
mongoOplogEnabled = oplogEnabled; mongoVersion = version;
} catch (e) { mongoStorageEngine = storageEngine.name;
try { mongoOplogEnabled = oplogEnabled;
const { version } = Promise.await(mongo.db.command({ buildinfo: 1 })); } catch (e) {
mongoVersion = version; try {
mongoStorageEngine = 'unknown'; const { version } = Promise.await(
} catch (e) { mongo.db.command({ buildinfo: 1 }),
mongoVersion = 'unknown'; );
mongoStorageEngine = 'unknown'; 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;
},
});

Loading…
Cancel
Save