From 100762ea2e2e3b0236edaea1782b55248d547b7e Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Sun, 15 Nov 2026 11:36:49 +0100 Subject: [PATCH] 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 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 --- web/www/sunders/camera.php | 123 ++++++++----------- web/www/sunders/js/leafletembed_functions.js | 39 ++++-- 2 files changed, 79 insertions(+), 83 deletions(-) diff --git a/web/www/sunders/camera.php b/web/www/sunders/camera.php index 97a77e7..9aa2f85 100644 --- a/web/www/sunders/camera.php +++ b/web/www/sunders/camera.php @@ -4,6 +4,17 @@ 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 { var $id; var $lat; @@ -179,63 +190,47 @@ || !array_key_exists('zoom', $_GET) || !array_key_exists('width', $_GET) || !array_key_exists('height', $_GET)) { - header('Content-type: application/json'); - $result = '{"error":"bbox, zoom, width and height parameters are required. ' + exitWithError('bbox, zoom, width and height parameters are required. ' .(array_key_exists('bbox', $_GET) ? '' : 'bbox is empty. ') .(array_key_exists('zoom', $_GET) ? '' : 'zoom is empty. ') .(array_key_exists('width', $_GET) ? '' : 'width is empty. ') - .(array_key_exists('height', $_GET) ? '' : 'height is empty. ') - .'"}'; - echo $result; - exit; + .(array_key_exists('height', $_GET) ? '' : 'height is empty. ')); } /* Check zoom */ $zoom = $_GET['zoom']; - if ( !is_numeric($zoom) + if ( !is_string($zoom) + || !is_numeric($zoom) || intval($zoom) < 1 || intval($zoom) > 19) { - header('Content-type: application/json'); - $result = '{"error":"unexpected zoom value : ' - .htmlentities($zoom) - .'"}'; - echo $result; - exit; + exitWithError('unexpected zoom value'); } /* 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']); - 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[1]) || $bbox[1] < -90 || $bbox[1] > 90 // 1 = SW.lat || !is_numeric($bbox[3]) || $bbox[3] < -90 || $bbox[3] > 90) { // 3 = NE.lat - header('Content-type: application/json'); - $result = '{"error":"unexpected bbox longitude and latitude values : ' - .'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).'], ' - .'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']' - .'"}'; - echo $result; - exit; + exitWithError('unexpected bbox longitude and latitude values : ' + .'lon ['.$bbox[0].', '.$bbox[2].'], ' + .'lat ['.$bbox[1].', '.$bbox[3].']'); } if ($bbox[0] >= $bbox[2]) { // 0 = SW.lng | 2 = NE.lng - header('Content-type: application/json'); - $result = '{"error":"min longitude greater than max longitude : ' - .'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).']' - .'"}'; - echo $result; - exit; + exitWithError('min longitude greater than max longitude : ' + .'lon ['.$bbox[0].', '.$bbox[2].']'); } if ($bbox[1] >= $bbox[3]) { // 1 = SW.lat | 3 = NE.lat - header('Content-type: application/json'); - $result = '{"error":"min latitude greater than max latitude : ' - .'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']' - .'"}'; - echo $result; - exit; + exitWithError('min latitude greater than max latitude : ' + .'lat ['.$bbox[1].', '.$bbox[3].']'); } $lonMin = $bbox[0]; // 0 = SW.lng @@ -247,12 +242,7 @@ $pixelHeight = (int) $_GET['height']; if ($pixelWidth == 0 || $pixelHeight == 0) { - header('Content-type: application/json'); - $result = '{"error":"width or height is null : ' - .htmlentities($pixelWidth).'x'.htmlentities($pixelHeight) - .'"}'; - echo $result; - exit; + exitWithError('width or height is null : '.$pixelWidth.'x'.$pixelHeight); } $lonWidth = $lonMax - $lonMin; // NE.lng - SW.lng @@ -268,10 +258,7 @@ /* Connect to database */ $mysqli = new mysqli(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB); if($mysqli->connect_errno) { - header('Content-type: application/json'); - $result = '{"error":"error while connecting to db : ' . $mysqli->error . '"}'; - echo $result; - exit; + exitWithError('error while connecting to db'); } $rqtLonMin = $lonMin; // SW.lng @@ -365,10 +352,7 @@ $stmt->close(); } else { $mysqli->close(); - header('Content-type: application/json'); - $result = '{"error":"Error while request : ' . $mysqli->error . '"}'; - echo $result; - exit; + exitWithError('error while querying db'); } /* Unify some clusters if nodes center are near */ @@ -380,8 +364,7 @@ } } - $result='['; - $separator=''; + $result = array(); /* Writing selected points */ $sql='SELECT k, v @@ -393,53 +376,49 @@ $stmt->bind_param('d', $id); $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) { if ($val['count'] > 0) { - $result = $result.$separator - .'{"lat":"'.($val['latitude'] / $val['count']).'"' - .',"lon":"'.($val['longitude'] / $val['count']).'"'; + $entry = array( + 'lat' => (string) ($val['latitude'] / $val['count']), + 'lon' => (string) ($val['longitude'] / $val['count'])); if ($val['count'] == 1) { $id = $val['points'][0]->id; - $result = $result.',"id":"'.$id.'"'; + $entry['id'] = (string) $id; $stmt->execute(); 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 { - $result = $result - .',"count":"'.$val['count'].'"' - .',"multi":"yes"' - .',"poly":['; + $entry['count'] = (string) $val['count']; + $entry['multi'] = 'yes'; + $entry['poly'] = array(); if ($pointsCount < MAX_POINTS_FOR_QUICKHULL) { $convexPoly = quickHull($val['points'], $val['count']); - $sepPoly = ''; foreach($convexPoly as $point) { - $result = $result.$sepPoly.'{' - .'"lat":"'.$point->lat.'"' - .',"id":"'.$point->id.'"' - .',"lon":"'.$point->lon.'"}'; - $sepPoly=','; + $entry['poly'][] = array( + 'lat' => (string) $point->lat, + 'id' => (string) $point->id, + 'lon' => (string) $point->lon); } } - $result=$result.']'; } - $result = $result.'}'; - $separator=','; + $result[] = $entry; } } $stmt->close(); - $result = $result . ']'; - $mysqli->close(); - header('Content-type: application/json; Charset : utf-8'); - echo $result; + header('Content-type: application/json; charset=utf-8'); + echo json_encode($result, JSON_FLAGS); ?> diff --git a/web/www/sunders/js/leafletembed_functions.js b/web/www/sunders/js/leafletembed_functions.js index cb199a0..75e461e 100644 --- a/web/www/sunders/js/leafletembed_functions.js +++ b/web/www/sunders/js/leafletembed_functions.js @@ -129,7 +129,11 @@ function onClick(e) { function onStateChanged() { 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 - plotList = eval('(' + ajaxRequest.responseText + ')'); + try { + plotList = JSON.parse(ajaxRequest.responseText); + } catch(e) { + return; + } removeMarkers(); for (i=0; i/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // Add camera popup to camera marker. +// All values in plot come unescaped from OSM data and must be escaped. function addCameraDetailsData(plotMarker, plot) { + var id = escapeHtml(plot.id); popupDataTable = '' - + '' - // + '' - + '' - + ''; + + '' + // + '' + + '' + + ''; for (x in plot) { if (plot[x] !== '' && x != 'multi' && x != 'id' && x != 'userid' && x != 'lat' && x != 'lon') { - popupDataTable = popupDataTable + ''; }