Skip to content

Commit 718eba2

Browse files
committed
Parse data: srcset candidates without swallowing later URLs
A descriptor on a later candidate must not extend a data: URL that has no descriptor of its own. Split on a comma-plus-space after the data payload so javascript: (and other) candidates are checked separately.
1 parent 91e279a commit 718eba2

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

js/src/util/sanitizer.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,45 @@ const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))
6868

6969
const isSafeUrl = url => Boolean(SAFE_URL_PATTERN.test(url))
7070

71-
// `srcset` is a comma-separated list of candidates. Commas also appear inside
72-
// `data:` URLs, so the whole attribute string cannot be checked as one URI.
71+
// `srcset` is a comma-separated list of candidates. The first comma in a
72+
// `data:` URL starts the payload; later commas (with surrounding space) start
73+
// the next candidate. Do not search the whole remainder for `1x`/`2x`/`w`.
7374
const SRCSET_DESCRIPTOR = /\s+[\d.]+[wx]\s*(?:,|$)/i
75+
const SRCSET_NEXT_CANDIDATE = /\s*,\s+(?=\S)/
7476

7577
const extractSrcsetUrls = value => {
7678
const urls = []
7779
let rest = String(value).trim()
7880

7981
while (rest) {
8082
if (/^data:/i.test(rest)) {
81-
const descriptor = rest.match(SRCSET_DESCRIPTOR)
83+
const headerComma = rest.indexOf(',')
8284

83-
if (descriptor) {
84-
urls.push(rest.slice(0, descriptor.index).trim())
85-
rest = rest.slice(descriptor.index + descriptor[0].length).replace(/^,/, '').trim()
86-
} else {
85+
if (headerComma === -1) {
8786
urls.push(rest)
88-
rest = ''
87+
break
88+
}
89+
90+
const payload = rest.slice(headerComma + 1)
91+
const descriptor = payload.match(SRCSET_DESCRIPTOR)
92+
const nextCandidate = payload.match(SRCSET_NEXT_CANDIDATE)
93+
const descriptorAt = descriptor ? descriptor.index : Number.POSITIVE_INFINITY
94+
const nextAt = nextCandidate ? nextCandidate.index : Number.POSITIVE_INFINITY
95+
96+
if (nextAt !== Number.POSITIVE_INFINITY && nextAt <= descriptorAt) {
97+
urls.push(rest.slice(0, headerComma + 1 + nextAt).trim())
98+
rest = payload.slice(nextAt).replace(/^,/, '').trim()
99+
continue
100+
}
101+
102+
if (descriptor) {
103+
urls.push(rest.slice(0, headerComma + 1 + descriptor.index).trim())
104+
rest = payload.slice(descriptor.index + descriptor[0].length).replace(/^,/, '').trim()
105+
continue
89106
}
90107

108+
urls.push(rest)
109+
rest = ''
91110
continue
92111
}
93112

js/tests/unit/util/sanitizer.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ describe('Sanitizer', () => {
190190
expect(result).not.toContain('srcset')
191191
})
192192

193+
it('should drop srcset when a data: candidate is followed by an unsafe candidate', () => {
194+
// eslint-disable-next-line no-script-url
195+
const unsafeSrcset = 'javascript:alert(1)'
196+
const template = `<img src="safe.jpg" srcset="data:image/png;base64,AAAA , ${unsafeSrcset} 2x">`
197+
198+
const result = sanitizeHtml(template, DefaultAllowlist, null)
199+
200+
expect(result).not.toContain('srcset')
201+
})
202+
193203
it('should keep a data-URI srcset whose commas belong to the payload', () => {
194204
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/'
195205
const template = `<img src="safe.jpg" srcset="${dataUrl} 1x">`

0 commit comments

Comments
 (0)