Skip to content

Commit 662f622

Browse files
Merge branch 'main' into sass-compile-include
2 parents 5b6e056 + 896c390 commit 662f622

File tree

8 files changed

+635
-509
lines changed

8 files changed

+635
-509
lines changed

js/src/scrollspy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,11 @@ class ScrollSpy extends BaseComponent {
208208
continue
209209
}
210210

211-
const observableSection = SelectorEngine.findOne(anchor.hash, this._element)
211+
const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)
212212

213213
// ensure that the observableSection exists & is visible
214214
if (isVisible(observableSection)) {
215-
this._targetLinks.set(anchor.hash, anchor)
215+
this._targetLinks.set(decodeURI(anchor.hash), anchor)
216216
this._observableSections.set(anchor.hash, observableSection)
217217
}
218218
}

js/src/util/sanitizer.js

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,6 @@
55
* --------------------------------------------------------------------------
66
*/
77

8-
const uriAttributes = new Set([
9-
'background',
10-
'cite',
11-
'href',
12-
'itemtype',
13-
'longdesc',
14-
'poster',
15-
'src',
16-
'xlink:href'
17-
])
18-
19-
/**
20-
* A pattern that recognizes a commonly useful subset of URLs that are safe.
21-
*
22-
* Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts
23-
*/
24-
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i
25-
26-
/**
27-
* A pattern that matches safe data URLs. Only matches image, video and audio types.
28-
*
29-
* Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts
30-
*/
31-
const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i
32-
33-
const allowedAttribute = (attribute, allowedAttributeList) => {
34-
const attributeName = attribute.nodeName.toLowerCase()
35-
36-
if (allowedAttributeList.includes(attributeName)) {
37-
if (uriAttributes.has(attributeName)) {
38-
return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue))
39-
}
40-
41-
return true
42-
}
43-
44-
// Check if a regular expression validates the attribute.
45-
return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
46-
.some(regex => regex.test(attributeName))
47-
}
48-
498
// js-docs-start allow-list
509
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
5110

@@ -84,6 +43,42 @@ export const DefaultAllowlist = {
8443
}
8544
// js-docs-end allow-list
8645

46+
const uriAttributes = new Set([
47+
'background',
48+
'cite',
49+
'href',
50+
'itemtype',
51+
'longdesc',
52+
'poster',
53+
'src',
54+
'xlink:href'
55+
])
56+
57+
/**
58+
* A pattern that recognizes URLs that are safe wrt. XSS in URL navigation
59+
* contexts.
60+
*
61+
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
62+
*/
63+
// eslint-disable-next-line unicorn/better-regex
64+
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i
65+
66+
const allowedAttribute = (attribute, allowedAttributeList) => {
67+
const attributeName = attribute.nodeName.toLowerCase()
68+
69+
if (allowedAttributeList.includes(attributeName)) {
70+
if (uriAttributes.has(attributeName)) {
71+
return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))
72+
}
73+
74+
return true
75+
}
76+
77+
// Check if a regular expression validates the attribute.
78+
return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
79+
.some(regex => regex.test(attributeName))
80+
}
81+
8782
export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
8883
if (!unsafeHtml.length) {
8984
return unsafeHtml
@@ -102,7 +97,6 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
10297

10398
if (!Object.keys(allowList).includes(elementName)) {
10499
element.remove()
105-
106100
continue
107101
}
108102

js/tests/unit/scrollspy.spec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,5 +940,39 @@ describe('ScrollSpy', () => {
940940
}, 100)
941941
link.click()
942942
})
943+
944+
it('should smoothscroll to observable with anchor link that contains a french word as id', done => {
945+
fixtureEl.innerHTML = [
946+
'<nav id="navBar" class="navbar">',
947+
' <ul class="nav">',
948+
' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#présentation">div 1</a></li>',
949+
' </ul>',
950+
'</nav>',
951+
'<div class="content" data-bs-target="#navBar" style="overflow-y: auto">',
952+
' <div id="présentation">div 1</div>',
953+
'</div>'
954+
].join('')
955+
956+
const div = fixtureEl.querySelector('.content')
957+
const link = fixtureEl.querySelector('[href="#présentation"]')
958+
const observable = fixtureEl.querySelector('#présentation')
959+
const clickSpy = getElementScrollSpy(div)
960+
// eslint-disable-next-line no-new
961+
new ScrollSpy(div, {
962+
offset: 1,
963+
smoothScroll: true
964+
})
965+
966+
setTimeout(() => {
967+
if (div.scrollTo) {
968+
expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' })
969+
} else {
970+
expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop)
971+
}
972+
973+
done()
974+
}, 100)
975+
link.click()
976+
})
943977
})
944978
})

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

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,75 @@ describe('Sanitizer', () => {
1010
expect(result).toEqual(empty)
1111
})
1212

13-
it('should sanitize template by removing tags with XSS', () => {
14-
const template = [
15-
'<div>',
16-
' <a href="javascript:alert(7)">Click me</a>',
17-
' <span>Some content</span>',
18-
'</div>'
19-
].join('')
20-
21-
const result = sanitizeHtml(template, DefaultAllowlist, null)
13+
it('should retain tags with valid URLs', () => {
14+
const validUrls = [
15+
'',
16+
'http://abc',
17+
'HTTP://abc',
18+
'https://abc',
19+
'HTTPS://abc',
20+
'ftp://abc',
21+
'FTP://abc',
22+
23+
24+
'tel:123-123-1234',
25+
'TEL:123-123-1234',
26+
27+
28+
'#anchor',
29+
'/page1.md',
30+
'http://JavaScript/my.js',
31+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
32+
'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
33+
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
34+
'unknown-scheme:abc'
35+
]
36+
37+
for (const url of validUrls) {
38+
const template = [
39+
'<div>',
40+
` <a href="${url}">Click me</a>`,
41+
' <span>Some content</span>',
42+
'</div>'
43+
].join('')
44+
45+
const result = sanitizeHtml(template, DefaultAllowlist, null)
46+
47+
expect(result).toContain(`href="${url}"`)
48+
}
49+
})
2250

23-
expect(result).not.toContain('href="javascript:alert(7)')
51+
it('should sanitize template by removing tags with XSS', () => {
52+
const invalidUrls = [
53+
// eslint-disable-next-line no-script-url
54+
'javascript:alert(7)',
55+
// eslint-disable-next-line no-script-url
56+
'javascript:evil()',
57+
// eslint-disable-next-line no-script-url
58+
'JavaScript:abc',
59+
' javascript:abc',
60+
' \n Java\n Script:abc',
61+
'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
62+
'&#106&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
63+
'&#106 &#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
64+
'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058',
65+
'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;',
66+
'jav&#x09;ascript:alert();',
67+
'jav\u0000ascript:alert();'
68+
]
69+
70+
for (const url of invalidUrls) {
71+
const template = [
72+
'<div>',
73+
` <a href="${url}">Click me</a>`,
74+
' <span>Some content</span>',
75+
'</div>'
76+
].join('')
77+
78+
const result = sanitizeHtml(template, DefaultAllowlist, null)
79+
80+
expect(result).not.toContain(`href="${url}"`)
81+
}
2482
})
2583

2684
it('should sanitize template and work with multiple regex', () => {

js/tests/visual/scrollspy.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<li><a class="dropdown-item" href="#one">One</a></li>
3030
<li><a class="dropdown-item" href="#two">Two</a></li>
3131
<li><a class="dropdown-item" href="#three">Three</a></li>
32+
<li><a class="dropdown-item" href="#présentation">Présentation</a></li>
3233
</ul>
3334
</li>
3435
<li class="nav-item">
@@ -82,6 +83,14 @@ <h2 id="three">three</h2>
8283
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
8384
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
8485
<hr>
86+
<h2 id="présentation">Présentation</h2>
87+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
88+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
89+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
90+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
91+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
92+
<p>Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat.</p>
93+
<hr>
8594
<h2 id="final">Final section</h2>
8695
<p>Ad leggings keytar, brunch id art party dolor labore.</p>
8796
</div>

0 commit comments

Comments
 (0)