Skip to content

Commit f4d4f11

Browse files
committed
fix(ssr): less aggressive encoding of non-title tags
Fixes #541
1 parent 2047b4c commit f4d4f11

File tree

3 files changed

+38
-4
lines changed

3 files changed

+38
-4
lines changed

packages/unhead/src/server/util/tagToString.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ export function tagToString<T extends HeadTag>(tag: T) {
3131
return SelfClosingTags.has(tag.tag) ? openTag : `${openTag}</${tag.tag}>`
3232

3333
// dangerously using innerHTML, we don't encode this
34-
let content = String(tag.innerHTML || '')
35-
if (tag.textContent)
36-
// content needs to be encoded to avoid XSS, only for title
37-
content = escapeHtml(String(tag.textContent))
34+
let content = String(tag.textContent || tag.innerHTML || '')
35+
content = tag.tag === 'title' ? escapeHtml(content) : content.replace(new RegExp(`<\/${tag.tag}`, 'gi'), `<\\/${tag.tag}`)
3836
return SelfClosingTags.has(tag.tag) ? openTag : `${openTag}${content}</${tag.tag}>`
3937
}

packages/unhead/test/unit/server/ssr.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHead } from '@unhead/dom'
12
import { describe, it } from 'vitest'
23
import { useHead, useSeoMeta } from '../../../src'
34
import { renderSSRHead } from '../../../src/server'
@@ -356,4 +357,24 @@ describe('ssr', () => {
356357
expect(processedHtml).toContain('<link rel="prefetch" href="another-script.js">')
357358
expect(processedHtml).toContain('<script src="another-script.js"></script>')
358359
})
360+
it('#541', async () => {
361+
const input = `
362+
<head>
363+
<style>
364+
html { background: url(/foo.png); }
365+
img::before { content: "foo"; }
366+
</style>
367+
</head>
368+
`
369+
const processedHtml = await transformHtmlTemplate(createHead(), input)
370+
expect(processedHtml).toMatchInlineSnapshot(`
371+
"
372+
<head>
373+
<style>
374+
html { background: url(/foo.png); }
375+
img::before { content: "foo"; }
376+
</style></head>
377+
"
378+
`)
379+
})
359380
})

packages/unhead/test/unit/server/xss.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,19 @@ describe('xss', () => {
351351
<link rel="stylesheet" href="vbscript:alert(1)">"
352352
`)
353353
})
354+
it('style tag', async () => {
355+
// body {color: red;}</style><script>alert('XSS')</script><style>
356+
const head = createServerHeadWithContext()
357+
head.push({
358+
style: [
359+
{ innerHTML: 'body {color: red;}</style><script>alert(\'XSS\')</script><style>' },
360+
{ innerHTML: '} </style><script>alert("XSS Attack Successful")</script><style>{</style>' },
361+
],
362+
})
363+
const ctx = await renderSSRHead(head)
364+
expect(ctx.headTags).toMatchInlineSnapshot(`
365+
"<style>body {color: red;}<\\/style><script>alert('XSS')</script><style></style>
366+
<style>} <\\/style><script>alert("XSS Attack Successful")</script><style>{<\\/style></style>"
367+
`)
368+
})
354369
})

0 commit comments

Comments
 (0)