forked from kamba4/sunders
fix: Escapes OSM tag data in camera popups
camera.php built its JSON by hand with htmlentities(), which leaves backslashes intact, and the client parsed it with eval() and inserted tag values into the popup as HTML. A tag value such as <img ...> therefore became live markup (stored XSS for anyone able to edit OSM data), and a lone backslash broke parsing of the whole response. - camera.php now builds the response with json_encode() and returns raw values - tags named lat, lon, id, count, multi or poly no longer override the fields set by the server - error responses no longer include database error messages - the client uses JSON.parse() and escapes all keys and values; only http(s) URLs are rendered as links/images
This commit is contained in:
parent
954b534b10
commit
100762ea2e
2 changed files with 79 additions and 83 deletions
|
|
@ -4,6 +4,17 @@
|
||||||
|
|
||||||
define('MAX_POINTS_FOR_QUICKHULL', 3000);
|
define('MAX_POINTS_FOR_QUICKHULL', 3000);
|
||||||
|
|
||||||
|
// Fields set by this script; OSM tags with these keys are not passed on.
|
||||||
|
define('RESERVED_KEYS', array('lat', 'lon', 'id', 'count', 'multi', 'poly'));
|
||||||
|
|
||||||
|
define('JSON_FLAGS', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||||
|
|
||||||
|
function exitWithError($message) {
|
||||||
|
header('Content-type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(array('error' => $message), JSON_FLAGS);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
class OsmPoint {
|
class OsmPoint {
|
||||||
var $id;
|
var $id;
|
||||||
var $lat;
|
var $lat;
|
||||||
|
|
@ -179,63 +190,47 @@
|
||||||
|| !array_key_exists('zoom', $_GET)
|
|| !array_key_exists('zoom', $_GET)
|
||||||
|| !array_key_exists('width', $_GET)
|
|| !array_key_exists('width', $_GET)
|
||||||
|| !array_key_exists('height', $_GET)) {
|
|| !array_key_exists('height', $_GET)) {
|
||||||
header('Content-type: application/json');
|
exitWithError('bbox, zoom, width and height parameters are required. '
|
||||||
$result = '{"error":"bbox, zoom, width and height parameters are required. '
|
|
||||||
.(array_key_exists('bbox', $_GET) ? '' : 'bbox is empty. ')
|
.(array_key_exists('bbox', $_GET) ? '' : 'bbox is empty. ')
|
||||||
.(array_key_exists('zoom', $_GET) ? '' : 'zoom is empty. ')
|
.(array_key_exists('zoom', $_GET) ? '' : 'zoom is empty. ')
|
||||||
.(array_key_exists('width', $_GET) ? '' : 'width is empty. ')
|
.(array_key_exists('width', $_GET) ? '' : 'width is empty. ')
|
||||||
.(array_key_exists('height', $_GET) ? '' : 'height is empty. ')
|
.(array_key_exists('height', $_GET) ? '' : 'height is empty. '));
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check zoom */
|
/* Check zoom */
|
||||||
$zoom = $_GET['zoom'];
|
$zoom = $_GET['zoom'];
|
||||||
|
|
||||||
if ( !is_numeric($zoom)
|
if ( !is_string($zoom)
|
||||||
|
|| !is_numeric($zoom)
|
||||||
|| intval($zoom) < 1
|
|| intval($zoom) < 1
|
||||||
|| intval($zoom) > 19) {
|
|| intval($zoom) > 19) {
|
||||||
header('Content-type: application/json');
|
exitWithError('unexpected zoom value');
|
||||||
$result = '{"error":"unexpected zoom value : '
|
|
||||||
.htmlentities($zoom)
|
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check bounds box --> bbox=boundsSW.lng,boundsSW.lat,boundsNE.lng,boundsNE.lat */
|
/* Check bounds box --> bbox=boundsSW.lng,boundsSW.lat,boundsNE.lng,boundsNE.lat */
|
||||||
|
if (!is_string($_GET['bbox'])) {
|
||||||
|
exitWithError('unexpected bbox value');
|
||||||
|
}
|
||||||
$bbox = explode(',', $_GET['bbox']);
|
$bbox = explode(',', $_GET['bbox']);
|
||||||
|
|
||||||
if ( !is_numeric($bbox[0]) || $bbox[0] > 180 // 0 = SW.lng
|
if ( count($bbox) != 4
|
||||||
|
|| !is_numeric($bbox[0]) || $bbox[0] > 180 // 0 = SW.lng
|
||||||
|| !is_numeric($bbox[2]) || $bbox[2] < -180 // 2 = NE.lng
|
|| !is_numeric($bbox[2]) || $bbox[2] < -180 // 2 = NE.lng
|
||||||
|| !is_numeric($bbox[1]) || $bbox[1] < -90 || $bbox[1] > 90 // 1 = SW.lat
|
|| !is_numeric($bbox[1]) || $bbox[1] < -90 || $bbox[1] > 90 // 1 = SW.lat
|
||||||
|| !is_numeric($bbox[3]) || $bbox[3] < -90 || $bbox[3] > 90) { // 3 = NE.lat
|
|| !is_numeric($bbox[3]) || $bbox[3] < -90 || $bbox[3] > 90) { // 3 = NE.lat
|
||||||
header('Content-type: application/json');
|
exitWithError('unexpected bbox longitude and latitude values : '
|
||||||
$result = '{"error":"unexpected bbox longitude and latitude values : '
|
.'lon ['.$bbox[0].', '.$bbox[2].'], '
|
||||||
.'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).'], '
|
.'lat ['.$bbox[1].', '.$bbox[3].']');
|
||||||
.'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']'
|
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($bbox[0] >= $bbox[2]) { // 0 = SW.lng | 2 = NE.lng
|
if ($bbox[0] >= $bbox[2]) { // 0 = SW.lng | 2 = NE.lng
|
||||||
header('Content-type: application/json');
|
exitWithError('min longitude greater than max longitude : '
|
||||||
$result = '{"error":"min longitude greater than max longitude : '
|
.'lon ['.$bbox[0].', '.$bbox[2].']');
|
||||||
.'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).']'
|
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($bbox[1] >= $bbox[3]) { // 1 = SW.lat | 3 = NE.lat
|
if ($bbox[1] >= $bbox[3]) { // 1 = SW.lat | 3 = NE.lat
|
||||||
header('Content-type: application/json');
|
exitWithError('min latitude greater than max latitude : '
|
||||||
$result = '{"error":"min latitude greater than max latitude : '
|
.'lat ['.$bbox[1].', '.$bbox[3].']');
|
||||||
.'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']'
|
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$lonMin = $bbox[0]; // 0 = SW.lng
|
$lonMin = $bbox[0]; // 0 = SW.lng
|
||||||
|
|
@ -247,12 +242,7 @@
|
||||||
$pixelHeight = (int) $_GET['height'];
|
$pixelHeight = (int) $_GET['height'];
|
||||||
|
|
||||||
if ($pixelWidth == 0 || $pixelHeight == 0) {
|
if ($pixelWidth == 0 || $pixelHeight == 0) {
|
||||||
header('Content-type: application/json');
|
exitWithError('width or height is null : '.$pixelWidth.'x'.$pixelHeight);
|
||||||
$result = '{"error":"width or height is null : '
|
|
||||||
.htmlentities($pixelWidth).'x'.htmlentities($pixelHeight)
|
|
||||||
.'"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$lonWidth = $lonMax - $lonMin; // NE.lng - SW.lng
|
$lonWidth = $lonMax - $lonMin; // NE.lng - SW.lng
|
||||||
|
|
@ -268,10 +258,7 @@
|
||||||
/* Connect to database */
|
/* Connect to database */
|
||||||
$mysqli = new mysqli(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB);
|
$mysqli = new mysqli(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB);
|
||||||
if($mysqli->connect_errno) {
|
if($mysqli->connect_errno) {
|
||||||
header('Content-type: application/json');
|
exitWithError('error while connecting to db');
|
||||||
$result = '{"error":"error while connecting to db : ' . $mysqli->error . '"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$rqtLonMin = $lonMin; // SW.lng
|
$rqtLonMin = $lonMin; // SW.lng
|
||||||
|
|
@ -365,10 +352,7 @@
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
} else {
|
} else {
|
||||||
$mysqli->close();
|
$mysqli->close();
|
||||||
header('Content-type: application/json');
|
exitWithError('error while querying db');
|
||||||
$result = '{"error":"Error while request : ' . $mysqli->error . '"}';
|
|
||||||
echo $result;
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unify some clusters if nodes center are near */
|
/* Unify some clusters if nodes center are near */
|
||||||
|
|
@ -380,8 +364,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result='[';
|
$result = array();
|
||||||
$separator='';
|
|
||||||
|
|
||||||
/* Writing selected points */
|
/* Writing selected points */
|
||||||
$sql='SELECT k, v
|
$sql='SELECT k, v
|
||||||
|
|
@ -393,53 +376,49 @@
|
||||||
$stmt->bind_param('d', $id);
|
$stmt->bind_param('d', $id);
|
||||||
$stmt->bind_result($k, $v);
|
$stmt->bind_result($k, $v);
|
||||||
|
|
||||||
/* Writing grouped items or not */
|
/* Writing grouped items or not. All values are passed on unescaped; the client
|
||||||
|
must escape them before inserting them into HTML. */
|
||||||
foreach($resArray as $val) {
|
foreach($resArray as $val) {
|
||||||
if ($val['count'] > 0) {
|
if ($val['count'] > 0) {
|
||||||
$result = $result.$separator
|
$entry = array(
|
||||||
.'{"lat":"'.($val['latitude'] / $val['count']).'"'
|
'lat' => (string) ($val['latitude'] / $val['count']),
|
||||||
.',"lon":"'.($val['longitude'] / $val['count']).'"';
|
'lon' => (string) ($val['longitude'] / $val['count']));
|
||||||
|
|
||||||
if ($val['count'] == 1) {
|
if ($val['count'] == 1) {
|
||||||
$id = $val['points'][0]->id;
|
$id = $val['points'][0]->id;
|
||||||
$result = $result.',"id":"'.$id.'"';
|
$entry['id'] = (string) $id;
|
||||||
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
while($stmt->fetch()) {
|
while($stmt->fetch()) {
|
||||||
$result = $result.',"'.htmlentities($k).'":"'.htmlentities($v, ENT_COMPAT, 'UTF-8').'"';
|
if (!in_array($k, RESERVED_KEYS, true)) {
|
||||||
|
$entry[$k] = (string) $v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$result = $result
|
$entry['count'] = (string) $val['count'];
|
||||||
.',"count":"'.$val['count'].'"'
|
$entry['multi'] = 'yes';
|
||||||
.',"multi":"yes"'
|
$entry['poly'] = array();
|
||||||
.',"poly":[';
|
|
||||||
|
|
||||||
if ($pointsCount < MAX_POINTS_FOR_QUICKHULL) {
|
if ($pointsCount < MAX_POINTS_FOR_QUICKHULL) {
|
||||||
$convexPoly = quickHull($val['points'], $val['count']);
|
$convexPoly = quickHull($val['points'], $val['count']);
|
||||||
$sepPoly = '';
|
|
||||||
foreach($convexPoly as $point) {
|
foreach($convexPoly as $point) {
|
||||||
$result = $result.$sepPoly.'{'
|
$entry['poly'][] = array(
|
||||||
.'"lat":"'.$point->lat.'"'
|
'lat' => (string) $point->lat,
|
||||||
.',"id":"'.$point->id.'"'
|
'id' => (string) $point->id,
|
||||||
.',"lon":"'.$point->lon.'"}';
|
'lon' => (string) $point->lon);
|
||||||
$sepPoly=',';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$result=$result.']';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $result.'}';
|
$result[] = $entry;
|
||||||
$separator=',';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
|
|
||||||
$result = $result . ']';
|
|
||||||
|
|
||||||
$mysqli->close();
|
$mysqli->close();
|
||||||
|
|
||||||
header('Content-type: application/json; Charset : utf-8');
|
header('Content-type: application/json; charset=utf-8');
|
||||||
echo $result;
|
echo json_encode($result, JSON_FLAGS);
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,11 @@ function onClick(e) {
|
||||||
function onStateChanged() {
|
function onStateChanged() {
|
||||||
if (ajaxRequest.readyState == 4) { // 0 = UNSENT, 1 = OPENED, 2 = HEADERS_RECEIVED, 3 = LOADING, 4 = DONE
|
if (ajaxRequest.readyState == 4) { // 0 = UNSENT, 1 = OPENED, 2 = HEADERS_RECEIVED, 3 = LOADING, 4 = DONE
|
||||||
if (ajaxRequest.status == 200) { // 200 = OK, 404 = Page not found
|
if (ajaxRequest.status == 200) { // 200 = OK, 404 = Page not found
|
||||||
plotList = eval('(' + ajaxRequest.responseText + ')');
|
try {
|
||||||
|
plotList = JSON.parse(ajaxRequest.responseText);
|
||||||
|
} catch(e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
removeMarkers();
|
removeMarkers();
|
||||||
for (i=0; i<plotList.length; i++) {
|
for (i=0; i<plotList.length; i++) {
|
||||||
var plotLatLng;
|
var plotLatLng;
|
||||||
|
|
@ -417,26 +421,39 @@ function drawCameraFocusDome(plotLatLng, focusRadius, focusColor) {
|
||||||
return(new L.Circle(plotLatLng, focusRadius, { weight:1, color:focusColor, fillOpacity:0.1 }));
|
return(new L.Circle(plotLatLng, focusRadius, { weight:1, color:focusColor, fillOpacity:0.1 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape a value for use in HTML text and in double-quoted attributes.
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
// Add camera popup to camera marker.
|
// Add camera popup to camera marker.
|
||||||
|
// All values in plot come unescaped from OSM data and must be escaped.
|
||||||
function addCameraDetailsData(plotMarker, plot) {
|
function addCameraDetailsData(plotMarker, plot) {
|
||||||
|
var id = escapeHtml(plot.id);
|
||||||
popupDataTable = '<table class="popup-content">'
|
popupDataTable = '<table class="popup-content">'
|
||||||
+ '<tr><td>id</td><td><a href="https://www.openstreetmap.org/node/' + (plot.id) + '">' + (plot.id) + '</a></td></tr>'
|
+ '<tr><td>id</td><td><a href="https://www.openstreetmap.org/node/' + encodeURIComponent(plot.id) + '">' + id + '</a></td></tr>'
|
||||||
// + '<tr><td>user osm</td><td>' + (plot.userid) + '</td></tr>'
|
// + '<tr><td>user osm</td><td>' + escapeHtml(plot.userid) + '</td></tr>'
|
||||||
+ '<tr><td>latitude</td><td>' + (plot.lat) + '</td></tr>'
|
+ '<tr><td>latitude</td><td>' + escapeHtml(plot.lat) + '</td></tr>'
|
||||||
+ '<tr><td>longitude</td><td>' + (plot.lon) + '</td></tr>';
|
+ '<tr><td>longitude</td><td>' + escapeHtml(plot.lon) + '</td></tr>';
|
||||||
for (x in plot) {
|
for (x in plot) {
|
||||||
if (plot[x] !== '' && x != 'multi' && x != 'id' && x != 'userid' && x != 'lat' && x != 'lon') {
|
if (plot[x] !== '' && x != 'multi' && x != 'id' && x != 'userid' && x != 'lat' && x != 'lon') {
|
||||||
popupDataTable = popupDataTable + '<tr><td>' + x + '</td><td>';
|
popupDataTable = popupDataTable + '<tr><td>' + escapeHtml(x) + '</td><td>';
|
||||||
var descr = plot[x];
|
var descr = String(plot[x]);
|
||||||
if (descr.substr(0, 4) == 'http') {
|
if (/^https?:\/\//i.test(descr)) {
|
||||||
|
var url = escapeHtml(descr);
|
||||||
var suffix = descr.slice(-3).toLowerCase();
|
var suffix = descr.slice(-3).toLowerCase();
|
||||||
if (suffix == 'jpg' || suffix == 'gif' || suffix == 'png') {
|
if (suffix == 'jpg' || suffix == 'gif' || suffix == 'png') {
|
||||||
popupDataTable = popupDataTable + '<a href="' + descr + '"><img alt="Link" src="' + descr + '" width="200"/></a>';
|
popupDataTable = popupDataTable + '<a href="' + url + '"><img alt="Link" src="' + url + '" width="200"/></a>';
|
||||||
} else {
|
} else {
|
||||||
popupDataTable = popupDataTable + '<a href="' + descr + '">Link</a>';
|
popupDataTable = popupDataTable + '<a href="' + url + '">Link</a>';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
popupDataTable = popupDataTable + plot[x];
|
popupDataTable = popupDataTable + escapeHtml(descr);
|
||||||
}
|
}
|
||||||
popupDataTable = popupDataTable + '</td></tr>';
|
popupDataTable = popupDataTable + '</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue