Merge pull request #1094 from umap-project/use-dompurify

Use DOMPurify to escape malicious input from user
This commit is contained in:
David Larlet 2023-05-30 15:49:29 -04:00 committed by GitHub
commit 0cd1cf4ffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 54 additions and 19 deletions

View file

@ -42,6 +42,7 @@ vendors:
mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/ mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/
mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml
mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/{dist/L.Control.Locate.css,src/L.Control.Locate.js} umap/static/umap/vendors/locatecontrol/ mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/{dist/L.Control.Locate.css,src/L.Control.Locate.js} umap/static/umap/vendors/locatecontrol/
mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.js umap/static/umap/vendors/dompurify/
installjs: installjs:
npm install npm install
testjsfx: testjsfx:

11
package-lock.json generated
View file

@ -10,6 +10,7 @@
"license": "WTFPL", "license": "WTFPL",
"dependencies": { "dependencies": {
"csv2geojson": "5.1.1", "csv2geojson": "5.1.1",
"dompurify": "^3.0.3",
"georsstogeojson": "^0.1.0", "georsstogeojson": "^0.1.0",
"leaflet": "1.3.4", "leaflet": "1.3.4",
"leaflet-contextmenu": "^1.4.0", "leaflet-contextmenu": "^1.4.0",
@ -676,6 +677,11 @@
"domelementtype": "1" "domelementtype": "1"
} }
}, },
"node_modules/dompurify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.3.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.3.0.tgz",
@ -3719,6 +3725,11 @@
"domelementtype": "1" "domelementtype": "1"
} }
}, },
"dompurify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
},
"domutils": { "domutils": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.3.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.3.0.tgz",

View file

@ -36,6 +36,7 @@
"homepage": "http://wiki.openstreetmap.org/wiki/UMap", "homepage": "http://wiki.openstreetmap.org/wiki/UMap",
"dependencies": { "dependencies": {
"csv2geojson": "5.1.1", "csv2geojson": "5.1.1",
"dompurify": "^3.0.3",
"georsstogeojson": "^0.1.0", "georsstogeojson": "^0.1.0",
"leaflet": "1.3.4", "leaflet": "1.3.4",
"leaflet-contextmenu": "^1.4.0", "leaflet-contextmenu": "^1.4.0",

View file

@ -44,7 +44,28 @@ L.Util.setNullableBooleanFromQueryString = (options, name) => {
} }
L.Util.escapeHTML = (s) => { L.Util.escapeHTML = (s) => {
s = s ? s.toString() : '' s = s ? s.toString() : ''
return s.replace(/</gm, '&lt;') s = DOMPurify.sanitize(s, {
USE_PROFILES: { html: true },
ADD_TAGS: ['iframe'],
ALLOWED_TAGS: [
'h3',
'h4',
'h5',
'hr',
'strong',
'em',
'ul',
'li',
'a',
'div',
'iframe',
'img',
'br',
],
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
ALLOWED_ATTR: ['href', 'src', 'width', 'height'],
})
return s
} }
L.Util.toHTML = (r) => { L.Util.toHTML = (r) => {
if (!r) return '' if (!r) return ''
@ -53,9 +74,6 @@ L.Util.toHTML = (r) => {
// detect newline format // detect newline format
const newline = r.indexOf('\r\n') != -1 ? '\r\n' : r.indexOf('\n') != -1 ? '\n' : '' const newline = r.indexOf('\r\n') != -1 ? '\r\n' : r.indexOf('\n') != -1 ? '\n' : ''
// Escape tags
r = r.replace(/</gm, '&lt;')
// headings and hr // headings and hr
r = r.replace(/^### (.*)/gm, '<h5>$1</h5>') r = r.replace(/^### (.*)/gm, '<h5>$1</h5>')
r = r.replace(/^## (.*)/gm, '<h4>$1</h4>') r = r.replace(/^## (.*)/gm, '<h4>$1</h4>')
@ -109,6 +127,8 @@ L.Util.toHTML = (r) => {
// Preserver line breaks // Preserver line breaks
if (newline) r = r.replace(new RegExp(`${newline}(?=[^]+)`, 'g'), `<br>${newline}`) if (newline) r = r.replace(new RegExp(`${newline}(?=[^]+)`, 'g'), `<br>${newline}`)
r = L.Util.escapeHTML(r)
return r return r
} }
L.Util.isObject = (what) => typeof what === 'object' && what !== null L.Util.isObject = (what) => typeof what === 'object' && what !== null

View file

@ -38,49 +38,49 @@ describe('L.Util', function () {
it('should handle links without formatting', function () { it('should handle links without formatting', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple http://osm.org link'), L.Util.toHTML('A simple http://osm.org link'),
'A simple <a target="_blank" href="http://osm.org">http://osm.org</a> link' 'A simple <a href="http://osm.org" target="_blank">http://osm.org</a> link'
) )
}) })
it('should handle simple link in title', function () { it('should handle simple link in title', function () {
assert.equal( assert.equal(
L.Util.toHTML('# http://osm.org'), L.Util.toHTML('# http://osm.org'),
'<h3><a target="_blank" href="http://osm.org">http://osm.org</a></h3>' '<h3><a href="http://osm.org" target="_blank">http://osm.org</a></h3>'
) )
}) })
it('should handle links with url parameter', function () { it('should handle links with url parameter', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple https://osm.org/?url=https%3A//anotherurl.com link'), L.Util.toHTML('A simple https://osm.org/?url=https%3A//anotherurl.com link'),
'A simple <a target="_blank" href="https://osm.org/?url=https%3A//anotherurl.com">https://osm.org/?url=https%3A//anotherurl.com</a> link' 'A simple <a href="https://osm.org/?url=https%3A//anotherurl.com" target="_blank">https://osm.org/?url=https%3A//anotherurl.com</a> link'
) )
}) })
it('should handle simple link inside parenthesis', function () { it('should handle simple link inside parenthesis', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple link (http://osm.org)'), L.Util.toHTML('A simple link (http://osm.org)'),
'A simple link (<a target="_blank" href="http://osm.org">http://osm.org</a>)' 'A simple link (<a href="http://osm.org" target="_blank">http://osm.org</a>)'
) )
}) })
it('should handle simple link with formatting', function () { it('should handle simple link with formatting', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple [[http://osm.org]] link'), L.Util.toHTML('A simple [[http://osm.org]] link'),
'A simple <a target="_blank" href="http://osm.org">http://osm.org</a> link' 'A simple <a href="http://osm.org" target="_blank">http://osm.org</a> link'
) )
}) })
it('should handle simple link with formatting and content', function () { it('should handle simple link with formatting and content', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple [[http://osm.org|link]]'), L.Util.toHTML('A simple [[http://osm.org|link]]'),
'A simple <a target="_blank" href="http://osm.org">link</a>' 'A simple <a href="http://osm.org" target="_blank">link</a>'
) )
}) })
it('should handle simple link followed by a carriage return', function () { it('should handle simple link followed by a carriage return', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple link http://osm.org\nAnother line'), L.Util.toHTML('A simple link http://osm.org\nAnother line'),
'A simple link <a target="_blank" href="http://osm.org">http://osm.org</a><br>\nAnother line' 'A simple link <a href="http://osm.org" target="_blank">http://osm.org</a><br>\nAnother line'
) )
}) })
@ -108,28 +108,28 @@ describe('L.Util', function () {
it('should handle iframe', function () { it('should handle iframe', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html}}}'), L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html}}}'),
'A simple iframe: <div><iframe frameborder="0" src="http://osm.org/pouet.html" width="100%" height="300px"></iframe></div>' 'A simple iframe: <div><iframe src="http://osm.org/pouet.html" width="100%" height="300px" frameborder="0"></iframe></div>'
) )
}) })
it('should handle iframe with height', function () { it('should handle iframe with height', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200}}}'), L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200}}}'),
'A simple iframe: <div><iframe frameborder="0" src="http://osm.org/pouet.html" width="100%" height="200px"></iframe></div>' 'A simple iframe: <div><iframe src="http://osm.org/pouet.html" width="100%" height="200px" frameborder="0"></iframe></div>'
) )
}) })
it('should handle iframe with height and width', function () { it('should handle iframe with height and width', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200*400}}}'), L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200*400}}}'),
'A simple iframe: <div><iframe frameborder="0" src="http://osm.org/pouet.html" width="400px" height="200px"></iframe></div>' 'A simple iframe: <div><iframe src="http://osm.org/pouet.html" width="400px" height="200px" frameborder="0"></iframe></div>'
) )
}) })
it('should handle iframe with height with px', function () { it('should handle iframe with height with px', function () {
assert.equal( assert.equal(
L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200px}}}'), L.Util.toHTML('A simple iframe: {{{http://osm.org/pouet.html|200px}}}'),
'A simple iframe: <div><iframe frameborder="0" src="http://osm.org/pouet.html" width="100%" height="200px"></iframe></div>' 'A simple iframe: <div><iframe src="http://osm.org/pouet.html" width="100%" height="200px" frameborder="0"></iframe></div>'
) )
}) })
@ -138,7 +138,7 @@ describe('L.Util', function () {
L.Util.toHTML( L.Util.toHTML(
'A simple iframe: {{{https://osm.org/?url=https%3A//anotherurl.com}}}' 'A simple iframe: {{{https://osm.org/?url=https%3A//anotherurl.com}}}'
), ),
'A simple iframe: <div><iframe frameborder="0" src="https://osm.org/?url=https%3A//anotherurl.com" width="100%" height="300px"></iframe></div>' 'A simple iframe: <div><iframe src="https://osm.org/?url=https%3A//anotherurl.com" width="100%" height="300px" frameborder="0"></iframe></div>'
) )
}) })
@ -147,7 +147,7 @@ describe('L.Util', function () {
L.Util.toHTML( L.Util.toHTML(
'A double iframe: {{{https://osm.org/pouet}}}{{{https://osm.org/boudin}}}' 'A double iframe: {{{https://osm.org/pouet}}}{{{https://osm.org/boudin}}}'
), ),
'A double iframe: <div><iframe frameborder="0" src="https://osm.org/pouet" width="100%" height="300px"></iframe></div><div><iframe frameborder="0" src="https://osm.org/boudin" width="100%" height="300px"></iframe></div>' 'A double iframe: <div><iframe src="https://osm.org/pouet" width="100%" height="300px" frameborder="0"></iframe></div><div><iframe src="https://osm.org/boudin" width="100%" height="300px" frameborder="0"></iframe></div>'
) )
}) })
@ -156,14 +156,14 @@ describe('L.Util', function () {
L.Util.toHTML( L.Util.toHTML(
'A phrase with a [[http://iframeurl.com?to=http://another.com]].' 'A phrase with a [[http://iframeurl.com?to=http://another.com]].'
), ),
'A phrase with a <a target="_blank" href="http://iframeurl.com?to=http://another.com">http://iframeurl.com?to=http://another.com</a>.' 'A phrase with a <a href="http://iframeurl.com?to=http://another.com" target="_blank">http://iframeurl.com?to=http://another.com</a>.'
) )
}) })
}) })
describe('#escapeHTML', function () { describe('#escapeHTML', function () {
it('should escape HTML tags', function () { it('should escape HTML tags', function () {
assert.equal(L.Util.escapeHTML('<a href="pouet">'), '&lt;a href="pouet">') assert.equal(L.Util.escapeHTML('<span onload="alert(oups)">'), '<span></span>')
}) })
it('should not fail with int value', function () { it('should not fail with int value', function () {

View file

@ -22,6 +22,7 @@
<script src="../vendors/formbuilder/Leaflet.FormBuilder.js"></script> <script src="../vendors/formbuilder/Leaflet.FormBuilder.js"></script>
<script src="../vendors/measurable/Leaflet.Measurable.js"></script> <script src="../vendors/measurable/Leaflet.Measurable.js"></script>
<script src="../vendors/locatecontrol/L.Control.Locate.js"></script> <script src="../vendors/locatecontrol/L.Control.Locate.js"></script>
<script src="../vendors/dompurify/purify.js"></script>
<script src="../js/umap.core.js"></script> <script src="../js/umap.core.js"></script>
<script src="../js/umap.autocomplete.js"></script> <script src="../js/umap.autocomplete.js"></script>
<script src="../js/umap.popup.js"></script> <script src="../js/umap.popup.js"></script>

View file

@ -23,6 +23,7 @@
<script src="{{ STATIC_URL }}umap/vendors/togpx/togpx.js"></script> <script src="{{ STATIC_URL }}umap/vendors/togpx/togpx.js"></script>
<script src="{{ STATIC_URL }}umap/vendors/tokml/tokml.js"></script> <script src="{{ STATIC_URL }}umap/vendors/tokml/tokml.js"></script>
<script src="{{ STATIC_URL }}umap/vendors/locatecontrol/L.Control.Locate.js"></script> <script src="{{ STATIC_URL }}umap/vendors/locatecontrol/L.Control.Locate.js"></script>
<script src="{{ STATIC_URL }}umap/vendors/dompurify/purify.js"></script>
{% endcompress %} {% endcompress %}
{% if locale %} {% if locale %}
<script src="{{ STATIC_URL }}umap/locale/{{ locale }}.js"></script> <script src="{{ STATIC_URL }}umap/locale/{{ locale }}.js"></script>