Skip to content

Commit

Permalink
fix: use proper encoding for attr name / val pairs (#103)
Browse files Browse the repository at this point in the history
* fix: use proper encoding for attr name / val pairs

* fix: strip non-word characters
  • Loading branch information
harlan-zw committed Oct 1, 2022
1 parent f05e0f7 commit 669f520
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 10 deletions.
34 changes: 24 additions & 10 deletions src/stringify-attrs.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
// MIT licensed: modified from https://github.com/sindresorhus/stringify-attributes/blob/6e437781d684d9e61a6979a8dd2407a81dd3f4ed/index.js
const htmlEscape = (str: string) =>
/**
* Attribute names must consist of one or more characters other than controls, U+0020 SPACE, U+0022 ("), U+0027 ('),
* U+003E (>), U+002F (/), U+003D (=), and noncharacters.
*
* We strip them for the attribute name as they shouldn't exist even if encoded.
*
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
*/
export const stringifyAttrName = (str: string) =>
str
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
// replace special characters
.replace(/[\s"'><\/=]/g, "")
// replace noncharacters (except for - and _)
.replace(/[^a-zA-Z0-9_-]/g, "")
/**
* Double-quoted attribute value must not contain any literal U+0022 QUOTATION MARK characters ("). Including
* < and > will cause HTML to be invalid.
*
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
*/
export const stringifyAttrValue = (str: string) =>
str.replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")

export const stringifyAttrs = (attributes: any) => {
export const stringifyAttrs = (attributes: Record<string, any>) => {
const handledAttributes = []

for (let [key, value] of Object.entries(attributes)) {
Expand All @@ -19,10 +33,10 @@ export const stringifyAttrs = (attributes: any) => {
continue
}

let attribute = htmlEscape(key)
let attribute = stringifyAttrName(key)

if (value !== true) {
attribute += `="${htmlEscape(String(value))}"`
attribute += `="${stringifyAttrValue(String(value))}"`
}

handledAttributes.push(attribute)
Expand Down
69 changes: 69 additions & 0 deletions tests/encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { computed } from "vue"
import { createHead, renderHeadToString } from "../src"

describe("encoding", () => {
it("jailbreak", async () => {
const head = createHead()
head.addHeadObjs(
computed(() => ({
meta: [
{
['> console.alert("test")']:
"<style>body { background: red; }</style>",
},
],
})),
)
const { headTags } = renderHeadToString(head)
// valid html (except for the tag name)
expect(headTags).toMatchInlineSnapshot(
'"<meta consolealerttest=\\"&lt;style&gt;body { background: red; }&lt;/style&gt;\\"><meta name=\\"head:count\\" content=\\"1\\">"',
)
})

it("google maps", async () => {
const head = createHead()
head.addHeadObjs(
// @ts-expect-error computed issue
computed(() => ({
script: [
{
src: "https://polyfill.io/v3/polyfill.min.js?features=default",
},
{
src: "https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly",
["data-key"]: "AIzaSyD9hQ0Z7Y9XQX8Zjwq7Q9Z2YQ9Z2YQ9Z2Y",
defer: true,
body: true,
},
],
})),
)
const { headTags, bodyTags } = renderHeadToString(head)
// valid html
expect(headTags).toMatchInlineSnapshot(
'"<script src=\\"https://polyfill.io/v3/polyfill.min.js?features=default\\"></script><meta name=\\"head:count\\" content=\\"1\\">"',
)
// valid html
expect(bodyTags).toMatchInlineSnapshot(
'"<script src=\\"https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly\\" data-key=\\"AIzaSyD9hQ0Z7Y9XQX8Zjwq7Q9Z2YQ9Z2YQ9Z2Y\\" defer data-meta-body=\\"true\\"></script>"',
)
})

// Note: This should be fixed in a separate PR, possibly don't allow scripts without using useHeadRaw
it("xss", async () => {
const externalApiHeadData = {
script: [
{
children: 'console.alert("xss")',
},
],
}
const head = createHead()
head.addHeadObjs(computed(() => externalApiHeadData))
const { headTags } = renderHeadToString(head)
expect(headTags).toMatchInlineSnapshot(
'"<script>console.alert(\\"xss\\")</script><meta name=\\"head:count\\" content=\\"1\\">"',
)
})
})

0 comments on commit 669f520

Please sign in to comment.