Skip to content

Commit

Permalink
HTML-escape inputs before templating (#155)
Browse files Browse the repository at this point in the history
* Jinja filter

* Sanitize values before templating into map page

There was an XSS vulnerability allowing script tags to be injected via
the item or collection parameters, causing client side execution. Both
values are sanitized to avoid this vulnerability.

* Remove unused template

An unused template from the previous hard-coded mosaic era. Removing as
it does contain an XSS vulnerability were it to be reused.
  • Loading branch information
mmcfarland committed Mar 2, 2023
1 parent 26a67e1 commit 7dec823
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 102 deletions.
1 change: 1 addition & 0 deletions pccommon/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"types-cachetools==4.2.9",
"pyhumps==3.5.3",
"redis==4.2.0-rc1",
"html-sanitizer==1.9",
]

extra_reqs = {
Expand Down
15 changes: 11 additions & 4 deletions pctiler/pctiler/endpoints/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from starlette.responses import HTMLResponse
from titiler.core.factory import MultiBaseTilerFactory
from titiler.pgstac.dependencies import ItemPathParams
from html_sanitizer.sanitizer import Sanitizer

from pccommon.config import get_render_config
from pctiler.colormaps import PCColorMapParams
Expand Down Expand Up @@ -46,22 +47,28 @@ def map(
content=f"No item map available for collection {collection}",
)

qs = render_config.get_full_render_qs(collection, item)
# Sanitize collection and item to avoid XSS when the values are templated
# into the rendered html page
sanitizer = Sanitizer()
collection_sanitized = sanitizer.sanitize(collection)
item_sanitized = sanitizer.sanitize(item)

qs = render_config.get_full_render_qs(collection_sanitized, item_sanitized)
tilejson_url = pc_tile_factory.url_for(request, "tilejson")
tilejson_url += f"?{qs}"

item_url = urljoin(
get_settings().get_stac_api_href(request),
f"collections/{collection}/items/{item}",
f"collections/{collection_sanitized}/items/{item_sanitized}",
)

return templates.TemplateResponse(
"item_preview.html",
context={
"request": request,
"tileJson": tilejson_url,
"collectionId": collection,
"itemId": item,
"collectionId": collection_sanitized,
"itemId": item_sanitized,
"itemUrl": item_url,
},
)
107 changes: 55 additions & 52 deletions pctiler/pctiler/endpoints/templates/item_preview.html
Original file line number Diff line number Diff line change
@@ -1,60 +1,63 @@
<html lang="en">
<head>
<head>
<title>Planetary Computer STAC Item Preview</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin="" />
</head>

<body>
<div id="map" style="position:fixed;right:0px;left:0px;height:100%;">
</div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin="">
</script>
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""
/>
</head>

<body>
<div
id="map"
style="position: fixed; right: 0px; left: 0px; height: 100%"
></div>
<script
src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""
></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>

$.ajax({
url: '{{ tileJson|safe }}',
success: function (tileJson) {
var map = L.map('map', {
center: [tileJson.center[1], tileJson.center[0]],
zoom: 13,
});
var baseLayer = L.tileLayer(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}.png'
).addTo(map);

var tiles = tileJson.tiles[0];

var tileLayer = L.tileLayer(tiles, {
minZoom: tileJson.minzoon,
maxZoom: tileJson.maxzoom
$.ajax({
url: "{{ tileJson | safe }}",
success: function (tileJson) {
var map = L.map("map", {
center: [tileJson.center[1], tileJson.center[0]],
zoom: 13,
});
var baseLayer = L.tileLayer(
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}.png"
).addTo(map);

var tiles = tileJson.tiles[0];

var tileLayer = L.tileLayer(tiles, {
minZoom: tileJson.minzoon,
maxZoom: tileJson.maxzoom,
}).addTo(map);

$.ajax({
url: "{{ itemUrl }}",
success: function (item) {
map.whenReady(function () {
footprint = new L.GeoJSON(item);
L.geoJSON(item, {
style: {
color: "#05a6f0",
opacity: 0.7,
fillOpacity: 0,
},
}).addTo(map);

$.ajax({
url: '{{ itemUrl }}',
success: function (item) {
map.whenReady(function () {
footprint = new L.GeoJSON(item);
L.geoJSON(item, {
"style": {
"color": "#05a6f0",
"opacity": 0.7,
"fillOpacity": 0,

}
}).addTo(map);

map.fitBounds(footprint.getBounds());
});
}
});
}
});
map.fitBounds(footprint.getBounds());
});
},
});
},
});
</script>
</body>

</body>
</html>
46 changes: 0 additions & 46 deletions pctiler/pctiler/endpoints/templates/mosaic_preview.html

This file was deleted.

14 changes: 14 additions & 0 deletions pctiler/tests/endpoints/test_pg_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ async def test_item(client: AsyncClient) -> None:
)
assert response.status_code == 200
assert response.json() == ["image"]


@pytest.mark.asyncio
async def test_item_preview_xss(client: AsyncClient) -> None:
xss = "%27});alert('xss injected');//</script>"

item_id = "al_m_3008501_ne_16_060_20191109_20200114"
item_id_xss = f"{item_id}{xss}"

response_xss = await client.get(f"/item/map?collection=naip&item={item_id_xss}")

# The XSS should be sanitized out of the response
assert response_xss.status_code == 200
assert "//</script>" not in response_xss.text

0 comments on commit 7dec823

Please sign in to comment.