fix: prevent HTML injections to code blocks (#1165)

zio/stable
jviide 2023-01-15 12:48:22 +02:00 committed by GitHub
parent 1a4fd19720
commit c15df78cbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 25 deletions

View File

@ -18,5 +18,6 @@ const highlighted = computed(() => {
</script> </script>
<template> <template>
<pre class="code-block" v-html="highlighted" /> <pre v-if="lang" class="code-block" v-html="highlighted" />
<pre v-else class="code-block">{{ raw }}</pre>
</template> </template>

View File

@ -48,10 +48,22 @@ export function useShikiTheme() {
return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light' return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light'
} }
const HTML_ENTITIES = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'\'': '&apos;',
'"': '&quot;',
} as Record<string, string>
function escapeHtml(text: string) {
return text.replace(/[<>&'"]/g, ch => HTML_ENTITIES[ch])
}
export function highlightCode(code: string, lang: Lang) { export function highlightCode(code: string, lang: Lang) {
const shiki = useHightlighter(lang) const shiki = useHightlighter(lang)
if (!shiki) if (!shiki)
return code return escapeHtml(code)
return shiki.codeToHtml(code, { return shiki.codeToHtml(code, {
lang, lang,

View File

@ -1,9 +1,36 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`content-rich > block with backticks 1`] = `"<p><pre>[(\`number string) (\`tag string)]</pre></p>"`; exports[`content-rich > block with backticks 1`] = `"<p><pre class=\\"code-block\\">[(\`number string) (\`tag string)]</pre></p>"`;
exports[`content-rich > block with injected html, with a known language 1`] = `
"<pre>
<code class=\\"language-js\\">
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > block with injected html, with an unknown language 1`] = `
"<pre>
<code class=\\"language-xyzzy\\">
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > block with injected html, without language 1`] = `
"<pre>
<code>
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > code frame 1`] = ` exports[`content-rich > code frame 1`] = `
"<p>Testing code block</p><p></p><p><pre lang=\\"ts\\">import { useMouse, usePreferredDark } from &#39;@vueuse/core&#39; "<p>Testing code block</p><p></p><p><pre class=\\"code-block\\">import { useMouse, usePreferredDark } from &apos;@vueuse/core&apos;
// tracks mouse position // tracks mouse position
const { x, y } = useMouse() const { x, y } = useMouse()
// is the user prefers dark theme // is the user prefers dark theme
@ -20,14 +47,14 @@ exports[`content-rich > code frame 2 1`] = `
></a ></a
></span> ></span>
Testing<br /> Testing<br />
<pre lang=\\"ts\\">const a = hello</pre> <pre class=\\"code-block\\">const a = hello</pre>
</p> </p>
" "
`; `;
exports[`content-rich > code frame empty 1`] = `"<p><pre></pre><br></p>"`; exports[`content-rich > code frame empty 1`] = `"<p><pre class=\\"code-block\\"></pre><br></p>"`;
exports[`content-rich > code frame no lang 1`] = `"<p><pre>hello world</pre><br>no lang</p>"`; exports[`content-rich > code frame no lang 1`] = `"<p><pre class=\\"code-block\\">hello world</pre><br>no lang</p>"`;
exports[`content-rich > custom emoji 1`] = ` exports[`content-rich > custom emoji 1`] = `
"Daniel Roe "Daniel Roe
@ -75,7 +102,7 @@ exports[`content-rich > handles formatting from servers 1`] = `
exports[`content-rich > handles html within code blocks 1`] = ` exports[`content-rich > handles html within code blocks 1`] = `
"<p> "<p>
HTML block code:<br /> HTML block code:<br />
<pre lang=\\"html\\"> <pre class=\\"code-block\\">
&lt;span class=&quot;icon--noto icon--noto--1st-place-medal&quot;&gt;&lt;/span&gt; &lt;span class=&quot;icon--noto icon--noto--1st-place-medal&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;icon--noto icon--noto--2nd-place-medal-medal&quot;&gt;&lt;/span&gt;</pre &lt;span class=&quot;icon--noto icon--noto--2nd-place-medal-medal&quot;&gt;&lt;/span&gt;</pre
> >

View File

@ -136,6 +136,39 @@ describe('content-rich', () => {
" "
`) `)
}) })
it ('block with injected html, without language', async () => {
const { formatted } = await render(`
<pre>
<code>
&lt;a href="javascript:alert(1)">click me&lt;/a>
</code>
</pre>
`)
expect(formatted).toMatchSnapshot()
})
it ('block with injected html, with an unknown language', async () => {
const { formatted } = await render(`
<pre>
<code class="language-xyzzy">
&lt;a href="javascript:alert(1)">click me&lt;/a>
</code>
</pre>
`)
expect(formatted).toMatchSnapshot()
})
it ('block with injected html, with a known language', async () => {
const { formatted } = await render(`
<pre>
<code class="language-js">
&lt;a href="javascript:alert(1)">click me&lt;/a>
</code>
</pre>
`)
expect(formatted).toMatchSnapshot()
})
}) })
async function render(content: string, options?: ContentParseOptions) { async function render(content: string, options?: ContentParseOptions) {
@ -173,23 +206,11 @@ vi.mock('~/composables/dialog.ts', () => {
return {} return {}
}) })
vi.mock('~/components/content/ContentCode.vue', () => { vi.mock('shiki-es', async (importOriginal) => {
const mod = await importOriginal()
return { return {
default: defineComponent({ ...(mod as any),
props: { setCDN() {},
code: {
type: String,
required: true,
},
lang: {
type: String,
},
},
setup(props) {
const raw = computed(() => decodeURIComponent(props.code).replace(/&#39;/g, '\''))
return () => h('pre', { lang: props.lang }, raw.value)
},
}),
} }
}) })

View File

@ -14,7 +14,9 @@ export default defineConfig({
'process.client': 'true', 'process.client': 'true',
}, },
plugins: [ plugins: [
Vue(), Vue({
reactivityTransform: true,
}),
AutoImport({ AutoImport({
dts: false, dts: false,
imports: [ imports: [