Skip to content

Commit

Permalink
feat: support json content (without disabling sanitizers) (#415)
Browse files Browse the repository at this point in the history
* feat: add json prop to bypass sanitizers

* chore: fix lint

* feat: escape keys as well

test: fix json escaping

* add escapeKeys into escapeOptions
  • Loading branch information
pimlie authored Jul 24, 2019
1 parent fc71e1f commit 51fe6ea
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 12 deletions.
5 changes: 5 additions & 0 deletions src/client/updaters/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export default function updateTag (appId, options = {}, type, tags, head, body)
continue
}

if (attr === 'json') {
newElement.innerHTML = JSON.stringify(tag.json)
continue
}

if (attr === 'cssText') {
if (newElement.styleSheet) {
/* istanbul ignore next */
Expand Down
7 changes: 6 additions & 1 deletion src/server/generators/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
}

let json = ''
if (tag.json) {
json = JSON.stringify(tag.json)
}

// grab child content from one of these attributes, if possible
const content = tag.innerHTML || tag.cssText || ''
const content = tag.innerHTML || tag.cssText || json

// generate tag exactly without any other redundant attribute

Expand Down
2 changes: 1 addition & 1 deletion src/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const tagsWithoutEndTag = ['base', 'meta', 'link']
export const tagsWithInnerContent = ['noscript', 'script', 'style']

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

// Attributes which should be added with data- prefix
export const commonDataAttributes = ['body', 'pbody']
Expand Down
20 changes: 15 additions & 5 deletions src/shared/escaping.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const clientSequences = [
// sanitizes potentially dangerous characters
export function escape (info, options, escapeOptions) {
const { tagIDKeyName } = options
const { doEscape = v => v } = escapeOptions
const { doEscape = v => v, escapeKeys } = escapeOptions
const escaped = {}

for (const key in info) {
Expand Down Expand Up @@ -55,15 +55,25 @@ export function escape (info, options, escapeOptions) {
escaped[key] = doEscape(value)
} else if (isArray(value)) {
escaped[key] = value.map((v) => {
return isPureObject(v)
? escape(v, options, escapeOptions)
: doEscape(v)
if (isPureObject(v)) {
return escape(v, options, { ...escapeOptions, escapeKeys: true })
}

return doEscape(v)
})
} else if (isPureObject(value)) {
escaped[key] = escape(value, options, escapeOptions)
escaped[key] = escape(value, options, { ...escapeOptions, escapeKeys: true })
} else {
escaped[key] = value
}

if (escapeKeys) {
const escapedKey = doEscape(key)
if (key !== escapedKey) {
escaped[escapedKey] = escaped[key]
delete escaped[key]
}
}
}

return escaped
Expand Down
40 changes: 40 additions & 0 deletions test/unit/escaping.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _getMetaInfo from '../../src/shared/getMetaInfo'
import { loadVueMetaPlugin } from '../utils'
import { defaultOptions } from '../../src/shared/constants'
import { serverSequences } from '../../src/shared/escaping'

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

Expand Down Expand Up @@ -96,4 +97,43 @@ describe('escaping', () => {
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
})
})

test('json is still safely escaped', () => {
const component = new Vue({
metaInfo: {
script: [
{
json: {
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
'</script>unsafeKey': 'This is also still safe'
}
}
]
}
})

expect(getMetaInfo(component, serverSequences)).toEqual({
title: undefined,
titleChunk: '',
titleTemplate: '%s',
htmlAttrs: {},
headAttrs: {},
bodyAttrs: {},
meta: [],
base: [],
link: [],
style: [],
script: [
{
json: {
perfectlySave: '&lt;/script&gt;&lt;p class=&quot;unsafe&quot;&gt;This is safe&lt;/p&gt;&lt;script&gt;',
'&lt;/script&gt;unsafeKey': 'This is also still safe'
}
}
],
noscript: [],
__dangerouslyDisableSanitizers: [],
__dangerouslyDisableSanitizersByTagID: {}
})
})
})
21 changes: 16 additions & 5 deletions test/utils/meta-info-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,22 @@ const metaInfoData = {
{ src: 'src1', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content', callback: () => {} },
{ src: 'src-prepend', async: true, defer: false, pbody: true },
{ src: 'src2', async: false, defer: true, body: true },
{ src: 'src3', async: false, skip: true }
{ src: 'src3', async: false, skip: true },
{ type: 'application/ld+json',
json: {
'@context': 'http://schema.org',
'@type': 'Organization',
'name': 'MyApp',
'url': 'https://www.myurl.com',
'logo': 'https://www.myurl.com/images/logo.png'
}
}
],
expect: [
'<script data-vue-meta="ssr" src="src1" defer data-vmid="content" onload="this.__vm_l=1"></script>',
'<script data-vue-meta="ssr" src="src-prepend" async data-pbody="true"></script>',
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>'
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>',
'<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>'
],
test (side, defaultTest) {
return () => {
Expand All @@ -139,12 +149,13 @@ const metaInfoData = {
// ssr doesnt generate data-body tags
const bodyPrepended = this.expect[1]
const bodyAppended = this.expect[2]
this.expect = [this.expect[0]]
this.expect = [this.expect.shift(), this.expect.pop()]

const tags = defaultTest()
const html = tags.text()

expect(tags.text()).not.toContain(bodyPrepended)
expect(tags.text()).not.toContain(bodyAppended)
expect(html).not.toContain(bodyPrepended)
expect(html).not.toContain(bodyAppended)
}
}
}
Expand Down

0 comments on commit 51fe6ea

Please sign in to comment.