From f3b00027b5ea04ca2b11a08e167097413ba8f70c Mon Sep 17 00:00:00 2001 From: pimlie Date: Wed, 24 Jul 2019 10:32:41 +0200 Subject: [PATCH 1/4] feat: add json prop to bypass sanitizers --- src/client/updaters/tag.js | 5 +++++ src/server/generators/tag.js | 9 +++++++-- src/shared/constants.js | 2 +- test/utils/meta-info-data.js | 21 ++++++++++++++++----- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/client/updaters/tag.js b/src/client/updaters/tag.js index d51533f6..c4635e26 100644 --- a/src/client/updaters/tag.js +++ b/src/client/updaters/tag.js @@ -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 */ diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index fcb315da..f932f88a 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -23,7 +23,7 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {} if (tag.skip) { return tagsStr } - + const tagKeys = Object.keys(tag) if (tagKeys.length === 0) { @@ -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 diff --git a/src/shared/constants.js b/src/shared/constants.js index a347b246..abb59503 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -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'] diff --git a/test/utils/meta-info-data.js b/test/utils/meta-info-data.js index 82e42764..aee118a3 100644 --- a/test/utils/meta-info-data.js +++ b/test/utils/meta-info-data.js @@ -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: [ '', '', - '' + '', + '' ], test (side, defaultTest) { return () => { @@ -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) } } } From 223ab749dccf4520991484e47da8947a8cd7efef Mon Sep 17 00:00:00 2001 From: pimlie Date: Wed, 24 Jul 2019 10:35:03 +0200 Subject: [PATCH 2/4] chore: fix lint --- src/server/generators/tag.js | 2 +- test/utils/meta-info-data.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index f932f88a..f92ed1db 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -23,7 +23,7 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {} if (tag.skip) { return tagsStr } - + const tagKeys = Object.keys(tag) if (tagKeys.length === 0) { diff --git a/test/utils/meta-info-data.js b/test/utils/meta-info-data.js index aee118a3..a0a65e4f 100644 --- a/test/utils/meta-info-data.js +++ b/test/utils/meta-info-data.js @@ -119,11 +119,11 @@ const metaInfoData = { { 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", + '@context': 'http://schema.org', + '@type': 'Organization', + 'name': 'MyApp', + 'url': 'https://www.myurl.com', + 'logo': 'https://www.myurl.com/images/logo.png' } } ], From 2aa65cfe6fd5cfa00586cf6edee154d8f75db6da Mon Sep 17 00:00:00 2001 From: pimlie Date: Wed, 24 Jul 2019 11:08:40 +0200 Subject: [PATCH 3/4] feat: escape keys as well test: fix json escaping --- src/shared/escaping.js | 14 ++++++++++--- test/unit/escaping.test.js | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/shared/escaping.js b/src/shared/escaping.js index 8f9b123b..f19bbcd2 100644 --- a/src/shared/escaping.js +++ b/src/shared/escaping.js @@ -19,7 +19,7 @@ export const clientSequences = [ ] // sanitizes potentially dangerous characters -export function escape (info, options, escapeOptions) { +export function escape (info, options, escapeOptions, escapeKeys) { const { tagIDKeyName } = options const { doEscape = v => v } = escapeOptions const escaped = {} @@ -56,14 +56,22 @@ export function escape (info, options, escapeOptions) { } else if (isArray(value)) { escaped[key] = value.map((v) => { return isPureObject(v) - ? escape(v, options, escapeOptions) + ? escape(v, options, escapeOptions, true) : doEscape(v) }) } else if (isPureObject(value)) { - escaped[key] = escape(value, options, escapeOptions) + escaped[key] = escape(value, options, escapeOptions, true) } else { escaped[key] = value } + + if (escapeKeys) { + const escapedKey = doEscape(key) + if (key !== escapedKey) { + escaped[escapedKey] = escaped[key] + delete escaped[key] + } + } } return escaped diff --git a/test/unit/escaping.test.js b/test/unit/escaping.test.js index e4775a72..aff3fef3 100644 --- a/test/unit/escaping.test.js +++ b/test/unit/escaping.test.js @@ -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) @@ -96,4 +97,43 @@ describe('escaping', () => { __dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] } }) }) + + test('json is still safely escaped', () => { + const component = new Vue({ + metaInfo: { + script: [ + { + json: { + perfectlySave: '

This is safe

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: '</script><p class="unsafe">This is safe</p><script>', + '</script>unsafeKey': 'This is also still safe' + } + } + ], + noscript: [], + __dangerouslyDisableSanitizers: [], + __dangerouslyDisableSanitizersByTagID: {} + }) + }) }) From ce166174c957ac4f0d883a49e676e8587b68a452 Mon Sep 17 00:00:00 2001 From: pimlie Date: Wed, 24 Jul 2019 11:15:40 +0200 Subject: [PATCH 4/4] add escapeKeys into escapeOptions --- src/shared/escaping.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/shared/escaping.js b/src/shared/escaping.js index f19bbcd2..9afd6770 100644 --- a/src/shared/escaping.js +++ b/src/shared/escaping.js @@ -19,9 +19,9 @@ export const clientSequences = [ ] // sanitizes potentially dangerous characters -export function escape (info, options, escapeOptions, escapeKeys) { +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) { @@ -55,12 +55,14 @@ export function escape (info, options, escapeOptions, escapeKeys) { escaped[key] = doEscape(value) } else if (isArray(value)) { escaped[key] = value.map((v) => { - return isPureObject(v) - ? escape(v, options, escapeOptions, true) - : 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, true) + escaped[key] = escape(value, options, { ...escapeOptions, escapeKeys: true }) } else { escaped[key] = value }