diff --git a/app/api/server/v1/invites.js b/app/api/server/v1/invites.js index 9409458e3093a3a0d329af7d498c5f1b3ac08e63..fd17ec3661908154747467f164621831574fe95e 100644 --- a/app/api/server/v1/invites.js +++ b/app/api/server/v1/invites.js @@ -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); diff --git a/app/invites/server/functions/validateInviteToken.js b/app/invites/server/functions/validateInviteToken.js index 88d35fd5ccab9f1e011589541681dc73510a1b35..dda8add8b6123428edc05af495e2e3831e3b24fa 100644 --- a/app/invites/server/functions/validateInviteToken.js +++ b/app/invites/server/functions/validateInviteToken.js @@ -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' }); } diff --git a/app/lib/server/methods/getFullUserData.js b/app/lib/server/methods/getFullUserData.js index 07ae554acb4e3e3525fe7db77ad5a22b875e5d5e..3c551dacd1ee53a7011d15c216c0ae48b00fc739 100644 --- a/app/lib/server/methods/getFullUserData.js +++ b/app/lib/server/methods/getFullUserData.js @@ -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(); }, }); diff --git a/app/lib/server/methods/getServerInfo.js b/app/lib/server/methods/getServerInfo.js index 2c76421adb5a4d4b97808cf6731424b13a3e1c92..4445eaf36f3580017e6f9520c0c885e977fc1aff 100644 --- a/app/lib/server/methods/getServerInfo.js +++ b/app/lib/server/methods/getServerInfo.js @@ -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; }, }); diff --git a/app/livechat/server/methods/loadHistory.js b/app/livechat/server/methods/loadHistory.js index 395ea3ea5a94d6d2c9da0cd5c192ff065a404c71..0ac3331e217a9a2f5c3cfa36e114b4d847ae57e5 100644 --- a/app/livechat/server/methods/loadHistory.js +++ b/app/livechat/server/methods/loadHistory.js @@ -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) { diff --git a/app/livechat/server/methods/saveOfficeHours.js b/app/livechat/server/methods/saveOfficeHours.js index a84b3b7a2fe43785b56339b1390892d744061686..d0e16a59843bc63c50402cc8bf7cf42b348459f9 100644 --- a/app/livechat/server/methods/saveOfficeHours.js +++ b/app/livechat/server/methods/saveOfficeHours.js @@ -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, diff --git a/app/markdown/lib/parser/marked/marked.js b/app/markdown/lib/parser/marked/marked.js index c330bddbaab618fbcec2fc7e8c08c5746cd11221..e6923893b335d77afbfc81324a0970c9c325640b 100644 --- a/app/markdown/lib/parser/marked/marked.js +++ b/app/markdown/lib/parser/marked/marked.js @@ -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; }; diff --git a/app/markdown/lib/parser/original/markdown.js b/app/markdown/lib/parser/original/markdown.js index f7b0279e6c442d7b92dd418b8fa0f959b690bb1b..637037ac57d6d42f914044ca6056d189cb32fa24 100644 --- a/app/markdown/lib/parser/original/markdown.js +++ b/app/markdown/lib/parser/original/markdown.js @@ -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  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(/&/g, '&'); - let escapedUrl = url; - escapedUrl = escapedUrl.replace(/&/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(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => { - if (!validateUrl(url)) { + msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\\/\\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, '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; }; diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 3d6e175f1de044535624d7d9a9971c71110437b2..fd92206771d84ef965f2cbc640ec14357cb9769d 100644 --- a/app/markdown/tests/client.tests.js +++ b/app/markdown/tests/client.tests.js @@ -157,7 +157,7 @@ const link = { '<http://link|Text>': escapeHTML('<http://link|Text>'), '<https://open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<https://open.rocket.chat/|Open Site For Rocket.Chat>'), '<https://open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML('<https://open.rocket.chat/ | Open Site For Rocket.Chat>'), - '<https://rocket.chat/|Rocket.Chat Site>': escapeHTML('<https://rocket.chat/|Rocket.Chat Site>'), + '<https://rocket.chat/|Rocket.Chat Site>': '&lt;https://rocket.chat/|Rocket.Chat Site&gt;', '<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': escapeHTML('<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>'), '<http://linkText>': escapeHTML('<http://linkText>'), '<https:open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML('<https:open.rocket.chat/ | Open Site For Rocket.Chat>'), @@ -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¶m2=2)': linkWrapped('http://link?param1=1¶m2=2', 'Text'), + '[Text](http://link?param1=1¶m2=2)': linkWrapped('http://link?param1=1&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'), }; diff --git a/app/message-mark-as-unread/server/unreadMessages.js b/app/message-mark-as-unread/server/unreadMessages.js index 246a33e087d5bdaab27be257c973bc7fc9040dfe..aec92fcf94b6dc165ee1b50a74939f64eb240a0e 100644 --- a/app/message-mark-as-unread/server/unreadMessages.js +++ b/app/message-mark-as-unread/server/unreadMessages.js @@ -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, diff --git a/app/message-pin/client/pinMessage.js b/app/message-pin/client/pinMessage.js index 9fbc2f778edcf0643f76401748509039892887b8..5843be4de181b752e8efd789b76e662ac7f57b25 100644 --- a/app/message-pin/client/pinMessage.js +++ b/app/message-pin/client/pinMessage.js @@ -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, diff --git a/app/message-pin/server/pinMessage.js b/app/message-pin/server/pinMessage.js index a62ef8b7138f6e2322d0c9562a9731d265a774aa..4543496ad2a073575cf0c51c20facf61e723313f 100644 --- a/app/message-pin/server/pinMessage.js +++ b/app/message-pin/server/pinMessage.js @@ -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); } diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css index 0b2e791a5cc65eb0c8735c26d380def2d2610df6..b34decf221cb8403ca23e42cb4874f459622390b 100644 --- a/app/theme/client/imports/general/base_old.css +++ b/app/theme/client/imports/general/base_old.css @@ -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); diff --git a/client/components/MarkdownText.js b/client/components/MarkdownText.js index eba2d322148a8fdbc22b6a70389fa09f8b19a2e3..1adc18fd751bdc1b9a335bf85a7ffb1b289cd458 100644 --- a/client/components/MarkdownText.js +++ b/client/components/MarkdownText.js @@ -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; diff --git a/package-lock.json b/package-lock.json index faa94118ceba943b18f16ca65ea8efd69eb41337..b833e288a0e9f5395003f1709d4a86332bc1a616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a68fa685f672ff169c9e4515acb0142370e6b4a9..2329dd975f8116ac068ea589c0b79c3c206931e9 100644 --- a/package.json +++ b/package.json @@ -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",