Skip to content

Commit 51fe6ea

Browse files
authored
feat: support json content (without disabling sanitizers) (#415)
* feat: add json prop to bypass sanitizers * chore: fix lint * feat: escape keys as well test: fix json escaping * add escapeKeys into escapeOptions
1 parent fc71e1f commit 51fe6ea

File tree

6 files changed

+83
-12
lines changed

6 files changed

+83
-12
lines changed

src/client/updaters/tag.js

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export default function updateTag (appId, options = {}, type, tags, head, body)
5656
continue
5757
}
5858

59+
if (attr === 'json') {
60+
newElement.innerHTML = JSON.stringify(tag.json)
61+
continue
62+
}
63+
5964
if (attr === 'cssText') {
6065
if (newElement.styleSheet) {
6166
/* istanbul ignore next */

src/server/generators/tag.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
6262
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
6363
}
6464

65+
let json = ''
66+
if (tag.json) {
67+
json = JSON.stringify(tag.json)
68+
}
69+
6570
// grab child content from one of these attributes, if possible
66-
const content = tag.innerHTML || tag.cssText || ''
71+
const content = tag.innerHTML || tag.cssText || json
6772

6873
// generate tag exactly without any other redundant attribute
6974

src/shared/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const tagsWithoutEndTag = ['base', 'meta', 'link']
9090
export const tagsWithInnerContent = ['noscript', 'script', 'style']
9191

9292
// Attributes which are inserted as childNodes instead of HTMLAttribute
93-
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText']
93+
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']
9494

9595
// Attributes which should be added with data- prefix
9696
export const commonDataAttributes = ['body', 'pbody']

src/shared/escaping.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const clientSequences = [
2121
// sanitizes potentially dangerous characters
2222
export function escape (info, options, escapeOptions) {
2323
const { tagIDKeyName } = options
24-
const { doEscape = v => v } = escapeOptions
24+
const { doEscape = v => v, escapeKeys } = escapeOptions
2525
const escaped = {}
2626

2727
for (const key in info) {
@@ -55,15 +55,25 @@ export function escape (info, options, escapeOptions) {
5555
escaped[key] = doEscape(value)
5656
} else if (isArray(value)) {
5757
escaped[key] = value.map((v) => {
58-
return isPureObject(v)
59-
? escape(v, options, escapeOptions)
60-
: doEscape(v)
58+
if (isPureObject(v)) {
59+
return escape(v, options, { ...escapeOptions, escapeKeys: true })
60+
}
61+
62+
return doEscape(v)
6163
})
6264
} else if (isPureObject(value)) {
63-
escaped[key] = escape(value, options, escapeOptions)
65+
escaped[key] = escape(value, options, { ...escapeOptions, escapeKeys: true })
6466
} else {
6567
escaped[key] = value
6668
}
69+
70+
if (escapeKeys) {
71+
const escapedKey = doEscape(key)
72+
if (key !== escapedKey) {
73+
escaped[escapedKey] = escaped[key]
74+
delete escaped[key]
75+
}
76+
}
6777
}
6878

6979
return escaped

test/unit/escaping.test.js

+40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import _getMetaInfo from '../../src/shared/getMetaInfo'
22
import { loadVueMetaPlugin } from '../utils'
33
import { defaultOptions } from '../../src/shared/constants'
4+
import { serverSequences } from '../../src/shared/escaping'
45

56
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences)
67

@@ -96,4 +97,43 @@ describe('escaping', () => {
9697
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
9798
})
9899
})
100+
101+
test('json is still safely escaped', () => {
102+
const component = new Vue({
103+
metaInfo: {
104+
script: [
105+
{
106+
json: {
107+
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
108+
'</script>unsafeKey': 'This is also still safe'
109+
}
110+
}
111+
]
112+
}
113+
})
114+
115+
expect(getMetaInfo(component, serverSequences)).toEqual({
116+
title: undefined,
117+
titleChunk: '',
118+
titleTemplate: '%s',
119+
htmlAttrs: {},
120+
headAttrs: {},
121+
bodyAttrs: {},
122+
meta: [],
123+
base: [],
124+
link: [],
125+
style: [],
126+
script: [
127+
{
128+
json: {
129+
perfectlySave: '&lt;/script&gt;&lt;p class=&quot;unsafe&quot;&gt;This is safe&lt;/p&gt;&lt;script&gt;',
130+
'&lt;/script&gt;unsafeKey': 'This is also still safe'
131+
}
132+
}
133+
],
134+
noscript: [],
135+
__dangerouslyDisableSanitizers: [],
136+
__dangerouslyDisableSanitizersByTagID: {}
137+
})
138+
})
99139
})

test/utils/meta-info-data.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,22 @@ const metaInfoData = {
116116
{ src: 'src1', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content', callback: () => {} },
117117
{ src: 'src-prepend', async: true, defer: false, pbody: true },
118118
{ src: 'src2', async: false, defer: true, body: true },
119-
{ src: 'src3', async: false, skip: true }
119+
{ src: 'src3', async: false, skip: true },
120+
{ type: 'application/ld+json',
121+
json: {
122+
'@context': 'http://schema.org',
123+
'@type': 'Organization',
124+
'name': 'MyApp',
125+
'url': 'https://www.myurl.com',
126+
'logo': 'https://www.myurl.com/images/logo.png'
127+
}
128+
}
120129
],
121130
expect: [
122131
'<script data-vue-meta="ssr" src="src1" defer data-vmid="content" onload="this.__vm_l=1"></script>',
123132
'<script data-vue-meta="ssr" src="src-prepend" async data-pbody="true"></script>',
124-
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>'
133+
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>',
134+
'<script data-vue-meta="ssr" type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"MyApp","url":"https://www.myurl.com","logo":"https://www.myurl.com/images/logo.png"}</script>'
125135
],
126136
test (side, defaultTest) {
127137
return () => {
@@ -139,12 +149,13 @@ const metaInfoData = {
139149
// ssr doesnt generate data-body tags
140150
const bodyPrepended = this.expect[1]
141151
const bodyAppended = this.expect[2]
142-
this.expect = [this.expect[0]]
152+
this.expect = [this.expect.shift(), this.expect.pop()]
143153

144154
const tags = defaultTest()
155+
const html = tags.text()
145156

146-
expect(tags.text()).not.toContain(bodyPrepended)
147-
expect(tags.text()).not.toContain(bodyAppended)
157+
expect(html).not.toContain(bodyPrepended)
158+
expect(html).not.toContain(bodyAppended)
148159
}
149160
}
150161
}

0 commit comments

Comments
 (0)