Security sync (#20430)

pull/20434/head^2
Diego Sampaio 4 years ago committed by GitHub
parent 2209451f30
commit ac9d7612a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/api/server/v1/invites.js
  2. 2
      app/invites/server/functions/validateInviteToken.js
  3. 7
      app/lib/server/methods/getFullUserData.js
  4. 5
      app/lib/server/methods/getServerInfo.js
  5. 4
      app/livechat/server/methods/loadHistory.js
  6. 8
      app/livechat/server/methods/saveOfficeHours.js
  7. 4
      app/markdown/lib/parser/marked/marked.js
  8. 27
      app/markdown/lib/parser/original/markdown.js
  9. 6
      app/markdown/tests/client.tests.js
  10. 11
      app/message-mark-as-unread/server/unreadMessages.js
  11. 10
      app/message-pin/client/pinMessage.js
  12. 50
      app/message-pin/server/pinMessage.js
  13. 4
      app/theme/client/imports/general/base_old.css
  14. 12
      client/components/MarkdownText.js
  15. 5
      package-lock.json
  16. 1
      package.json

@ -1,5 +1,3 @@
import { Meteor } from 'meteor/meteor';
import { API } from '../api';
import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite';
import { removeInvite } from '../../../invites/server/functions/removeInvite';
@ -46,10 +44,6 @@ API.v1.addRoute('validateInviteToken', { authRequired: false }, {
post() {
const { token } = this.bodyParams;
if (!token) {
throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' });
}
let valid = true;
try {
validateInviteToken(token);

@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { Invites, Rooms } from '../../../models';
export const validateInviteToken = (token) => {
if (!token) {
if (!token || typeof token !== 'string') {
throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' });
}

@ -4,7 +4,14 @@ import { getFullUserData } from '../functions';
Meteor.methods({
getFullUserData({ filter = '', username = '', limit = 1 }) {
console.warn('Method "getFullUserData" is deprecated and will be removed after v4.0.0');
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized');
}
const result = getFullUserData({ userId: Meteor.userId(), filter: filter || username, limit });
return result && result.fetch();
},
});

@ -4,6 +4,11 @@ import { Info } from '../../../utils';
Meteor.methods({
getServerInfo() {
if (!Meteor.userId()) {
console.warning('Method "getServerInfo" is deprecated and will be removed after v4.0.0');
throw new Meteor.Error('not-authorized');
}
return Info;
},
});

@ -5,6 +5,10 @@ import { LivechatVisitors } from '../../../models';
Meteor.methods({
'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) {
if (!token || typeof token !== 'string') {
return;
}
const visitor = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });
if (!visitor) {

@ -1,10 +1,16 @@
import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
import { LivechatBusinessHours } from '../../../models/server/raw';
Meteor.methods({
'livechat:saveOfficeHours'(day, start, finish, open) {
console.log('Method "livechat:saveOfficeHour" is deprecated and will be removed after v4.0.0');
console.warn('Method "livechat:saveOfficeHour" is deprecated and will be removed after v4.0.0');
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-business-hours')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveOfficeHours' });
}
LivechatBusinessHours.updateDayOfGlobalBusinessHour({
day,
start,

@ -1,7 +1,7 @@
import { Random } from 'meteor/random';
import _ from 'underscore';
import _marked from 'marked';
import dompurify from 'dompurify';
import hljs from '../../hljs';
import { escapeHTML } from '../../../../../lib/escapeHTML';
@ -107,5 +107,7 @@ export const marked = (message, {
highlight,
});
message.html = dompurify.sanitize(message.html);
return message;
};

@ -14,7 +14,17 @@ const addAsToken = (message, html) => {
return token;
};
const validateUrl = (url) => {
const validateUrl = (url, message) => {
// Don't render markdown inside links
if (message?.tokens?.some((token) => url.includes(token.token))) {
return false;
}
// Valid urls don't contain whitespaces
if (/\s/.test(url.trim())) {
return false;
}
try {
new URL(url);
return true;
@ -76,36 +86,37 @@ const parseNotEscaped = (message, {
// Support ![alt text](http://image url)
msg = msg.replace(new RegExp(`!\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\s]+)\\)`, 'gm'), (match, title, url) => {
if (!validateUrl(url)) {
if (!validateUrl(url, message)) {
return match;
}
url = encodeURI(url);
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
return addAsToken(message, `<a href="${ url }" title="${ title }" target="${ target }" rel="noopener noreferrer"><div class="inline-image" style="background-image: url(${ url });"></div></a>`);
});
// Support [Text](http://link)
msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\s]+)\\)`, 'gm'), (match, title, url) => {
if (!validateUrl(url)) {
if (!validateUrl(url, message)) {
return match;
}
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
title = title.replace(/&amp;/g, '&');
let escapedUrl = url;
escapedUrl = escapedUrl.replace(/&amp;/g, '&');
const escapedUrl = encodeURI(url);
return addAsToken(message, `<a href="${ escapedUrl }" target="${ target }" rel="noopener noreferrer">${ title }</a>`);
});
// Support <http://link|Text>
msg = msg.replace(new RegExp(`(?:<|&lt;)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|&gt;)(?:>|&gt;)`, 'gm'), (match, url, title) => {
if (!validateUrl(url)) {
msg = msg.replace(new RegExp(`(?:<|&lt;)((?:${ schemes }):\\\/\\\/[^\\|]+)\\|(.+?)(?=>|&gt;)(?:>|&gt;)`, 'gm'), (match, url, title) => {
if (!validateUrl(url, message)) {
return match;
}
url = encodeURI(url);
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
return addAsToken(message, `<a href="${ url }" target="${ target }" rel="noopener noreferrer">${ title }</a>`);
});
return msg;
};

@ -157,7 +157,7 @@ const link = {
'&lt;http://link|Text&gt;': escapeHTML('&lt;http://link|Text&gt;'),
'&lt;https://open.rocket.chat/|Open Site For Rocket.Chat&gt;': escapeHTML('&lt;https://open.rocket.chat/|Open Site For Rocket.Chat&gt;'),
'&lt;https://open.rocket.chat/ | Open Site For Rocket.Chat&gt;': escapeHTML('&lt;https://open.rocket.chat/ | Open Site For Rocket.Chat&gt;'),
'&lt;https://rocket.chat/|Rocket.Chat Site&gt;': escapeHTML('&lt;https://rocket.chat/|Rocket.Chat Site&gt;'),
'&lt;https://rocket.chat/|Rocket.Chat Site&gt;': '&amp;lt;https://rocket.chat/|Rocket.Chat Site&amp;gt;',
'&lt;https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;': escapeHTML('&lt;https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;'),
'&lt;http://linkText&gt;': escapeHTML('&lt;http://linkText&gt;'),
'&lt;https:open.rocket.chat/ | Open Site For Rocket.Chat&gt;': escapeHTML('&lt;https:open.rocket.chat/ | Open Site For Rocket.Chat&gt;'),
@ -172,7 +172,7 @@ const link = {
'<http://invalid link|Text>': escapeHTML('<http://invalid link|Text>'),
'<http://link|Text>': linkWrapped('http://link', 'Text'),
'<https://open.rocket.chat/|Open Site For Rocket.Chat>': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>': linkWrapped('https://open.rocket.chat/ ', ' Open Site For Rocket.Chat'),
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>': linkWrapped(encodeURI('https://open.rocket.chat/ '), ' Open Site For Rocket.Chat'),
'<https://rocket.chat/|Rocket.Chat Site>': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
'<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': linkWrapped('https://rocket.chat/docs/developer-guides/testing/#testing', 'Testing Entry on Rocket.Chat Docs Site'),
'<http://linkText>': escapeHTML('<http://linkText>'),
@ -199,7 +199,7 @@ const link = {
'[Rocket.Chat Site](tps://rocket.chat/)': '[Rocket.Chat Site](tps://rocket.chat/)',
'[Open Site For Rocket.Chat](open.rocket.chat/)': '[Open Site For Rocket.Chat](open.rocket.chat/)',
'[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)': '[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)',
'[Text](http://link?param1=1&param2=2)': linkWrapped('http://link?param1=1&param2=2', 'Text'),
'[Text](http://link?param1=1&param2=2)': linkWrapped('http://link?param1=1&amp;param2=2', 'Text'),
'[Testing Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation))': linkWrapped('https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)', 'Testing Double parentheses'),
'[Testing data after Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla)': linkWrapped('https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla', 'Testing data after Double parentheses'),
};

@ -12,8 +12,8 @@ Meteor.methods({
});
}
if (room) {
const lastMessage = Messages.findVisibleByRoomId({ rid: room, queryOptions: { limit: 1, sort: { ts: -1 } } }).fetch()[0];
if (room && typeof room === 'string') {
const lastMessage = Messages.findVisibleByRoomId(room, { limit: 1, sort: { ts: -1 } }).fetch()[0];
if (lastMessage == null) {
throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
@ -25,6 +25,13 @@ Meteor.methods({
return Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts);
}
if (typeof firstUnreadMessage?._id !== 'string') {
throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
method: 'unreadMessages',
action: 'Unread_messages',
});
}
const originalMessage = Messages.findOneById(firstUnreadMessage._id, {
fields: {
u: 1,

@ -19,9 +19,14 @@ Meteor.methods({
toastr.error(TAPi18n.__('error-pinning-message'));
return false;
}
if (typeof message._id !== 'string') {
toastr.error(TAPi18n.__('error-pinning-message'));
return false;
}
toastr.success(TAPi18n.__('Message_has_been_pinned'));
return ChatMessage.update({
_id: message._id,
rid: message.rid,
}, {
$set: {
pinned: true,
@ -41,9 +46,14 @@ Meteor.methods({
toastr.error(TAPi18n.__('error-unpinning-message'));
return false;
}
if (typeof message._id !== 'string') {
toastr.error(TAPi18n.__('error-unpinning-message'));
return false;
}
toastr.success(TAPi18n.__('Message_has_been_unpinned'));
return ChatMessage.update({
_id: message._id,
rid: message.rid,
}, {
$set: {
pinned: false,

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
@ -28,6 +29,8 @@ const shouldAdd = (attachments, attachment) => !attachments.some(({ message_link
Meteor.methods({
pinMessage(message, pinnedAt) {
check(message._id, String);
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
@ -42,30 +45,34 @@ Meteor.methods({
});
}
if (!hasPermission(Meteor.userId(), 'pin-message', message.rid)) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' });
let originalMessage = Messages.findOneById(message._id);
if (originalMessage == null || originalMessage._id == null) {
throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', {
method: 'pinMessage',
action: 'Message_pinning',
});
}
const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
const subscription = Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, Meteor.userId(), { fields: { _id: 1 } });
if (!subscription) {
return false;
}
let originalMessage = Messages.findOneById(message._id);
if (originalMessage == null || originalMessage._id == null) {
// If it's a valid message but on a room that the user is not subscribed to, report that the message was not found.
throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', {
method: 'pinMessage',
action: 'Message_pinning',
});
}
if (!hasPermission(Meteor.userId(), 'pin-message', originalMessage.rid)) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' });
}
const me = Users.findOneById(userId);
// If we keep history of edits, insert a new message to store history information
if (settings.get('Message_KeepHistory')) {
Messages.cloneAndSaveAsHistoryById(message._id, me);
}
const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId());
originalMessage.pinned = true;
originalMessage.pinnedAt = pinnedAt || Date.now;
@ -110,6 +117,8 @@ Meteor.methods({
);
},
unpinMessage(message) {
check(message._id, String);
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'unpinMessage',
@ -123,24 +132,27 @@ Meteor.methods({
});
}
if (!hasPermission(Meteor.userId(), 'pin-message', message.rid)) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' });
let originalMessage = Messages.findOneById(message._id);
if (originalMessage == null || originalMessage._id == null) {
throw new Meteor.Error('error-invalid-message', 'Message you are unpinning was not found', {
method: 'unpinMessage',
action: 'Message_pinning',
});
}
const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
const subscription = Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, Meteor.userId(), { fields: { _id: 1 } });
if (!subscription) {
return false;
}
let originalMessage = Messages.findOneById(message._id);
if (originalMessage == null || originalMessage._id == null) {
// If it's a valid message but on a room that the user is not subscribed to, report that the message was not found.
throw new Meteor.Error('error-invalid-message', 'Message you are unpinning was not found', {
method: 'unpinMessage',
action: 'Message_pinning',
});
}
if (!hasPermission(Meteor.userId(), 'pin-message', originalMessage.rid)) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'unpinMessage' });
}
const me = Users.findOneById(Meteor.userId());
// If we keep history of edits, insert a new message to store history information
@ -154,7 +166,7 @@ Meteor.methods({
username: me.username,
};
originalMessage = callbacks.run('beforeSaveMessage', originalMessage);
const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId());
if (isTheLastMessage(room, message)) {
Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned);
}

@ -1312,14 +1312,14 @@
.rc-old .container-bars {
position: relative;
z-index: 2;
margin: 5px 10px 0;
display: none;
visibility: hidden;
overflow: hidden;
flex-direction: column;
margin: 5px 10px 0;
transition: transform 0.4s ease, visibility 0.3s ease, opacity 0.3s ease;
transform: translateY(-10px);

@ -1,8 +1,7 @@
import { Box } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import marked from 'marked';
import { escapeHTML } from '../../lib/escapeHTML';
import dompurify from 'dompurify';
marked.InlineLexer.rules.gfm.strong = /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/;
marked.InlineLexer.rules.gfm.em = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/;
@ -13,9 +12,12 @@ const options = {
};
function MarkdownText({ content, preserveHtml = false, withRichContent = true, ...props }) {
const __html = useMemo(() => content && marked(preserveHtml ? content : escapeHTML(content), options), [content, preserveHtml]);
return __html ? <Box dangerouslySetInnerHTML={{ __html }} {...withRichContent && { withRichContent }} {...props} /> : null;
const sanitizer = dompurify.sanitize;
const __html = useMemo(() => {
const html = content && marked(content, options);
return preserveHtml ? html : html && sanitizer(html);
}, [content, preserveHtml, sanitizer]);
return __html ? <Box dangerouslySetInnerHTML={{ __html }} withRichContent={withRichContent} {...props} /> : null;
}
export default MarkdownText;

5
package-lock.json generated

@ -17691,6 +17691,11 @@
"domelementtype": "1"
}
},
"dompurify": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.6.tgz",
"integrity": "sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ=="
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",

@ -184,6 +184,7 @@
"core-js": "^3.8.1",
"cors": "^2.8.5",
"csv-parse": "^4.12.0",
"dompurify": "^2.2.6",
"ejson": "^2.2.0",
"emailreplyparser": "^0.0.5",
"emojione": "^4.5.0",

Loading…
Cancel
Save