From 3c6e1d1a8eef72e61ef1f6d5563c4517d52225de Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Mon, 19 Jun 2023 19:01:12 +0200 Subject: [PATCH] Escape HTML in local API text runs to prevent XSS (#3675) --- .../components/data-settings/data-settings.js | 8 ++------ src/renderer/helpers/api/local.js | 9 +++++++-- src/renderer/helpers/utils.js | 13 +++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index f9647bba4..877ac05ce 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -9,6 +9,7 @@ import { MAIN_PROFILE_ID } from '../../../constants' import { calculateColorLuminance, getRandomColor } from '../../helpers/colors' import { copyToClipboard, + escapeHTML, getTodayDateStrLocalTimezone, readFileFromDialog, showOpenDialog, @@ -587,12 +588,7 @@ export default defineComponent({ let opmlData = '' this.profileList[0].subscriptions.forEach((channel) => { - const escapedName = channel.name - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll('\'', ''') + const escapedName = escapeHTML(channel.name) const channelOpmlString = `` opmlData += channelOpmlString diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index 97c7164c0..0c5ea3e66 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -5,6 +5,7 @@ import { join } from 'path' import { PlayerCache } from './PlayerCache' import { CHANNEL_HANDLE_REGEX, + escapeHTML, extractNumberFromString, getUserDataPath, toLocalePublicationString @@ -551,8 +552,12 @@ export function parseLocalTextRuns(runs, emojiSize = 16, options = { looseChanne const parsedRuns = [] for (const run of runs) { + // may contain HTML, so we need to escape it, as we don't render unwanted HTML + // example: https://youtu.be/Hh_se2Zqsdk (see pinned comment) + const text = escapeHTML(run.text) + if (run instanceof Misc.EmojiRun) { - const { emoji, text } = run + const { emoji } = run // empty array if video creator removes a channel emoji so we ignore. // eg: pinned comment here https://youtu.be/v3wm83zoSSY @@ -577,7 +582,7 @@ export function parseLocalTextRuns(runs, emojiSize = 16, options = { looseChanne parsedRuns.push(`${altText}`) } } else { - const { text, bold, italics, strikethrough, endpoint } = run + const { bold, italics, strikethrough, endpoint } = run if (endpoint) { switch (endpoint.metadata.page_type) { diff --git a/src/renderer/helpers/utils.js b/src/renderer/helpers/utils.js index f85731e3c..d420a6914 100644 --- a/src/renderer/helpers/utils.js +++ b/src/renderer/helpers/utils.js @@ -646,3 +646,16 @@ export function getTodayDateStrLocalTimezone() { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString return timeNowStr.split('T')[0] } + +/** + * Escapes HTML tags to avoid XSS + * @param {string} untrusted + * @returns {string} + */ +export function escapeHTML(untrusted) { + return untrusted.replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('\'', ''') +}