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

@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
b  
.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-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")
select.js-outgoing-webhooks-type(name="type")
each _type in types
@ -257,7 +257,7 @@ template(name="outgoingWebhooksPopup")
input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}")
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-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
select.js-outgoing-webhooks-type(name="type")
@ -267,7 +267,10 @@ template(name="outgoingWebhooksPopup")
template(name="boardMenuPopup")
ul.pop-over-list
li: a.js-custom-fields {{_ 'custom-fields'}}
li
a.js-custom-fields
i.fa.fa-list-alt
| {{_ 'custom-fields'}}
li
a.js-open-archives
i.fa.fa-archive

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

@ -620,44 +620,6 @@ Users.mutations({
});
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) {
check(value, String);
Meteor.user().setListSortBy(value);
@ -678,51 +640,97 @@ Meteor.methods({
check(limit, Number);
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) {
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
inviteUserToBoard(username, boardId) {
check(username, String);
@ -754,8 +762,9 @@ if (Meteor.isServer) {
throw new Meteor.Error('error-user-notAllowSelf');
} else {
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');
}
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);

@ -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');
}
//});
},
});
},
});
}

@ -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;
},
});
},
});
}

Loading…
Cancel
Save