Skip to content

Commit dcd92aa

Browse files
Properly sanitize HTML (#8809)
* Properly sanitize HTML * Fix typo Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> * Keep title on img tags for Invidious comment emojis Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> --------- Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
1 parent 0b5e0e8 commit dcd92aa

File tree

17 files changed

+127
-31
lines changed

17 files changed

+127
-31
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default undefined

_scripts/webpack.renderer.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ const config = {
200200
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',
201201

202202
// Make @fortawesome/vue-fontawesome use the trimmed down API instead of the original @fortawesome/fontawesome-svg-core
203-
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js')
203+
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js'),
204+
205+
// Fix dompurify not being tree-shaking friendly
206+
dompurify$: path.resolve(__dirname, '_undefinedDefaultExport.mjs')
204207
},
205208
extensions: ['.js', '.vue']
206209
},

eslint.config.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ export default [
270270
rules: {
271271
'@stylistic/space-before-function-paren': 'off',
272272
'@stylistic/comma-dangle': ['error', 'only-multiline'],
273-
'vue/no-v-html': 'off',
273+
274+
// Ban v-html as it inserts HTML via innerHTML without sanitizing it
275+
// if inserting raw HTML is unavoidable the custom v-safer-html directive should be used
276+
// which sanitizes the HTML before inserting it into the DOM
277+
'vue/no-v-html': 'error',
274278

275279
'no-console': ['error', {
276280
allow: ['warn', 'error'],
@@ -342,6 +346,15 @@ export default [
342346
}
343347
}
344348
},
349+
{
350+
files: ['src/renderer/directives/vSaferHtml.js'],
351+
languageOptions: {
352+
globals: {
353+
// Fix Sanitizer not being listed in `globals` yet, remove it when it gets added in the future
354+
Sanitizer: 'readable'
355+
}
356+
}
357+
},
345358

346359
...eslintPluginJsonc.configs.base,
347360
{

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@seald-io/nedb": "^4.1.2",
5959
"autolinker": "^4.1.5",
6060
"bgutils-js": "^3.2.0",
61+
"dompurify": "^3.3.3",
6162
"electron-context-menu": "^4.1.1",
6263
"googlevideo": "^4.0.4",
6364
"marked": "^17.0.4",

src/renderer/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@
6666
</h1>
6767
</template>
6868
<bdo
69+
v-safer-html.lenient="updateChangelog"
6970
class="changeLogText"
7071
dir="ltr"
7172
lang="en"
72-
v-html="updateChangelog"
7373
/>
7474
<FtFlexBox>
7575
<FtButton
@@ -129,6 +129,7 @@ import FtPlaylistAddVideoPrompt from './components/FtPlaylistAddVideoPrompt/FtPl
129129
import FtCreatePlaylistPrompt from './components/FtCreatePlaylistPrompt/FtCreatePlaylistPrompt.vue'
130130
import FtKeyboardShortcutPrompt from './components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue'
131131
import FtSearchFilters from './components/FtSearchFilters/FtSearchFilters.vue'
132+
import { vSaferHtml } from './directives/vSaferHtml.js'
132133
133134
import store from './store/index'
134135

src/renderer/components/ChannelAbout/ChannelAbout.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
>
88
<h2>{{ $t("Channel.About.Channel Description") }}</h2>
99
<div
10+
v-safer-html="description"
1011
class="aboutInfo"
1112
dir="auto"
12-
v-html="description"
1313
/>
1414
</template>
1515
<template
@@ -121,6 +121,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'
121121
122122
import FtChannelBubble from '../../components/FtChannelBubble/FtChannelBubble.vue'
123123
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
124+
import { vSaferHtml } from '../../directives/vSaferHtml.js'
124125
125126
import store from '../../store/index'
126127

src/renderer/components/FtCommunityPost/FtCommunityPost.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@
5353
</p>
5454
</div>
5555
<p
56+
v-safer-html="postText"
5657
class="postText"
5758
dir="auto"
58-
v-html="postText"
5959
/>
6060
<swiper-container
6161
v-if="postType === 'multiImage' && postContent.content.length > 0"
@@ -177,6 +177,7 @@ import FtListVideo from '../ft-list-video/ft-list-video.vue'
177177
import FtListPlaylist from '../FtListPlaylist/FtListPlaylist.vue'
178178
import FtCommunityPoll from '../FtCommunityPoll/FtCommunityPoll.vue'
179179
import FtShareButton from '../FtShareButton/FtShareButton.vue'
180+
import { vSaferHtml } from '../../directives/vSaferHtml.js'
180181
181182
import store from '../../store/index'
182183

src/renderer/components/FtListChannel/FtListChannel.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@
6060
</div>
6161
<p
6262
v-if="listType !== 'grid'"
63+
v-safer-html="description"
6364
class="description"
6465
dir="auto"
65-
v-html="description"
6666
/>
6767
</div>
6868
<FtSubscribeButton
@@ -80,6 +80,7 @@
8080
import { computed } from 'vue'
8181
8282
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
83+
import { vSaferHtml } from '../../directives/vSaferHtml'
8384
8485
import store from '../../store/index'
8586

src/renderer/components/FtTimestampCatcher.vue

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
<template>
2+
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
23
<p
4+
v-safer-html="displayText"
35
dir="auto"
4-
@timestamp-clicked="catchTimestampClick"
5-
v-html="displayText"
6+
@click="catchTimestampClick"
67
/>
78
</template>
89

910
<script setup>
1011
import { computed } from 'vue'
1112
import { useRouter } from 'vue-router'
1213
14+
import { vSaferHtml } from '../directives/vSaferHtml.js'
15+
1316
const props = defineProps({
1417
inputHtml: {
1518
type: String,
@@ -40,15 +43,27 @@ const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):
4043
}).href
4144
4245
// Adding the URL lets the user open the video in a new window at this timestamp
43-
return `<a tabindex="${props.linkTabIndex}" href="${url}" onclick="event.preventDefault();this.dispatchEvent(new CustomEvent('timestamp-clicked',{bubbles:true,detail:${time}}));window.scrollTo(0,0)">${timestamp}</a>`
46+
return `<a tabindex="${props.linkTabIndex}" href="${url}" data-time="${time}">${timestamp}</a>`
4447
}))
4548
4649
const emit = defineEmits(['timestamp-event'])
4750
4851
/**
49-
* @param {CustomEvent} event
52+
* @param {PointerEvent} event
5053
*/
5154
function catchTimestampClick(event) {
52-
emit('timestamp-event', event.detail)
55+
/** @type {HTMLAnchorElement} */
56+
const target = event.target
57+
58+
if (target.tagName === 'A' && target.dataset.time) {
59+
const timeSeconds = parseInt(target.dataset.time)
60+
61+
if (!isNaN(timeSeconds)) {
62+
event.preventDefault()
63+
64+
emit('timestamp-event', timeSeconds)
65+
window.scrollTo(0, 0)
66+
}
67+
}
5368
}
5469
</script>

src/renderer/components/WatchVideoLiveChat/WatchVideoLiveChat.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@
136136
</p>
137137
</div>
138138
<p
139+
v-safer-html="superChat.message"
139140
class="chatMessage"
140141
dir="auto"
141-
v-html="superChat.message"
142142
/>
143143
</div>
144144
</div>
@@ -181,9 +181,9 @@
181181
</div>
182182
<p
183183
v-if="comment.message"
184+
v-safer-html="comment.message"
184185
class="chatMessage"
185186
dir="auto"
186-
v-html="comment.message"
187187
/>
188188
</template>
189189
<template
@@ -219,8 +219,8 @@
219219
>
220220
</span>
221221
<bdi
222+
v-safer-html="comment.message"
222223
class="chatMessage"
223-
v-html="comment.message"
224224
/>
225225
</p>
226226
</template>
@@ -255,6 +255,7 @@ import { YTNodes } from 'youtubei.js'
255255
import FtLoader from '../FtLoader/FtLoader.vue'
256256
import FtCard from '../ft-card/ft-card.vue'
257257
import FtButton from '../FtButton/FtButton.vue'
258+
import { vSaferHtml } from '../../directives/vSaferHtml.js'
258259
259260
import store from '../../store/index'
260261

0 commit comments

Comments
 (0)