Skip to content

Commit 7639c08

Browse files
committed
improve style attribute splitting
1 parent 654855b commit 7639c08

File tree

4 files changed

+94
-29
lines changed

4 files changed

+94
-29
lines changed

.changeset/proud-mugs-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'markdown-to-jsx': patch
3+
---
4+
5+
Improve splitting of style attributes.

index.compiler.spec.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,30 +1443,17 @@ describe('links', () => {
14431443
jest.spyOn(console, 'warn').mockImplementation(() => {})
14441444
jest.spyOn(console, 'error').mockImplementation(() => {})
14451445

1446-
// TODO: something is off on parsing here, because this prints:
1447-
// console.error("Warning: Unknown prop `javascript:alert` on <img> tag"...)
1448-
// Which it shouldn't
1449-
render(compiler('<img src="`<javascript:alert>`(\'alertstr\')"'))
1450-
expect(root.innerHTML).toMatchInlineSnapshot(`
1451-
<span>
1452-
<img>
1453-
\`('alertstr')"
1454-
</span>
1455-
`)
1446+
render(compiler('<img src="`<javascript:alert>`(\'alertstr\')" />'))
1447+
expect(root.innerHTML).toMatchInlineSnapshot(`<img>`)
14561448
expect(console.warn).toHaveBeenCalled()
14571449
})
14581450

14591451
it('should sanitize html images containing weird parsing src=s', () => {
14601452
jest.spyOn(console, 'warn').mockImplementation(() => {})
14611453
jest.spyOn(console, 'error').mockImplementation(() => {})
14621454

1463-
render(compiler('<img src="`<src="javascript:alert(`xss`)">`'))
1464-
expect(root.innerHTML).toMatchInlineSnapshot(`
1465-
<span>
1466-
<img src="\`<src=">
1467-
\`
1468-
</span>
1469-
`)
1455+
render(compiler('<img src="<src=\\"javascript:alert(`xss`)">'))
1456+
expect(root.innerHTML).toMatchInlineSnapshot(`<img>`)
14701457
expect(console.warn).toHaveBeenCalled()
14711458
})
14721459

@@ -1486,6 +1473,22 @@ describe('links', () => {
14861473
expect(console.warn).toHaveBeenCalled()
14871474
})
14881475

1476+
it('should not sanitize style attribute with an acceptable data image payload', () => {
1477+
jest.spyOn(console, 'warn').mockImplementation(() => {})
1478+
jest.spyOn(console, 'error').mockImplementation(() => {})
1479+
1480+
render(
1481+
compiler(
1482+
'<div style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==); color: red;">'
1483+
)
1484+
)
1485+
expect(root.innerHTML).toMatchInlineSnapshot(`
1486+
<div style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==); color: red;">
1487+
</div>
1488+
`)
1489+
expect(console.warn).not.toHaveBeenCalled()
1490+
})
1491+
14891492
it('should handle a link with a URL in the text', () => {
14901493
render(
14911494
compiler('[https://www.google.com *heck yeah*](http://www.google.com)')

index.tsx

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -753,28 +753,85 @@ function normalizeAttributeKey(key) {
753753
return key
754754
}
755755

756+
type StyleTuple = [key: string, value: string]
757+
758+
function parseStyleAttribute(styleString: string): StyleTuple[] {
759+
const styles: StyleTuple[] = []
760+
let buffer = ''
761+
let inUrl = false
762+
let inQuotes = false
763+
let quoteChar: '"' | "'" | '' = ''
764+
765+
if (!styleString) return styles
766+
767+
for (let i = 0; i < styleString.length; i++) {
768+
const char = styleString[i]
769+
770+
// Handle quotes
771+
if ((char === '"' || char === "'") && !inUrl) {
772+
if (!inQuotes) {
773+
inQuotes = true
774+
quoteChar = char
775+
} else if (char === quoteChar) {
776+
inQuotes = false
777+
quoteChar = ''
778+
}
779+
}
780+
781+
// Track url() values
782+
if (char === '(' && buffer.endsWith('url')) {
783+
inUrl = true
784+
} else if (char === ')' && inUrl) {
785+
inUrl = false
786+
}
787+
788+
// Only split on semicolons when not in quotes or url()
789+
if (char === ';' && !inQuotes && !inUrl) {
790+
const declaration = buffer.trim()
791+
if (declaration) {
792+
const colonIndex = declaration.indexOf(':')
793+
if (colonIndex > 0) {
794+
const key = declaration.slice(0, colonIndex).trim()
795+
const value = declaration.slice(colonIndex + 1).trim()
796+
styles.push([key, value])
797+
}
798+
}
799+
buffer = ''
800+
} else {
801+
buffer += char
802+
}
803+
}
804+
805+
// Handle the last declaration
806+
const declaration = buffer.trim()
807+
if (declaration) {
808+
const colonIndex = declaration.indexOf(':')
809+
if (colonIndex > 0) {
810+
const key = declaration.slice(0, colonIndex).trim()
811+
const value = declaration.slice(colonIndex + 1).trim()
812+
styles.push([key, value])
813+
}
814+
}
815+
816+
return styles
817+
}
818+
756819
function attributeValueToJSXPropValue(
757820
tag: MarkdownToJSX.HTMLTags,
758821
key: keyof React.AllHTMLAttributes<Element>,
759822
value: string,
760823
sanitizeUrlFn: MarkdownToJSX.Options['sanitizer']
761824
): any {
762825
if (key === 'style') {
763-
return value.split(/;\s?/).reduce(function (styles, kvPair) {
764-
const key = kvPair.slice(0, kvPair.indexOf(':'))
765-
826+
return parseStyleAttribute(value).reduce(function (styles, [key, value]) {
766827
// snake-case to camelCase
767828
// also handles PascalCasing vendor prefixes
768-
const camelCasedKey = key
769-
.trim()
770-
.replace(/(-[a-z])/g, substr => substr[1].toUpperCase())
829+
const camelCasedKey = key.replace(/(-[a-z])/g, substr =>
830+
substr[1].toUpperCase()
831+
)
771832

772833
// key.length + 1 to skip over the colon
773-
styles[camelCasedKey] = sanitizeUrlFn(
774-
kvPair.slice(key.length + 1).trim(),
775-
tag,
776-
key
777-
)
834+
styles[camelCasedKey] = sanitizeUrlFn(value, tag, key)
778835

779836
return styles
780837
}, {})
@@ -1633,7 +1690,6 @@ export function compiler(
16331690
order: Priority.HIGH,
16341691
parse(capture /*, parse, state*/) {
16351692
const tag = capture[1].trim() as MarkdownToJSX.HTMLTags
1636-
16371693
return {
16381694
attrs: attrStringToMap(tag, capture[2] || ''),
16391695
tag,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
}
111111
],
112112
"jest": {
113+
"clearMocks": true,
113114
"testEnvironment": "jsdom",
114115
"transform": {
115116
"^.+\\.[tj]sx?$": [

0 commit comments

Comments
 (0)