diff --git a/data_handler/utils/create_camera_update_statements.php b/data_handler/utils/create_camera_update_statements.php index cf18dfc..a2a072c 100755 --- a/data_handler/utils/create_camera_update_statements.php +++ b/data_handler/utils/create_camera_update_statements.php @@ -2,6 +2,18 @@ include "config.php"; +$seqNum = $_SERVER['argv'][1] ?? ''; +if (!ctype_digit($seqNum)) { + echo "Invalid sequence number\n"; + exit(1); +} + +// Quote a value for the generated SQL file. Values come from OSM data and are untrusted. +function sqlString($value) { + global $mysqli; + return "'" . $mysqli->real_escape_string((string) $value) . "'"; +} + $elementTypes = array(); $count = 0; $countDelete = 0; @@ -91,7 +103,7 @@ function endElement ($parser, $name) { $mode = ''; } else if ($name == 'node') { if ($mode == 'delete') { - $id = $curNodeAttrs['id']; + $id = (int) $curNodeAttrs['id']; if (! $deleteTagStmt->execute()) { echo "***** Error : Deleting tags $id : ". $deleteTagStmt->error . "\n"; @@ -119,7 +131,7 @@ function endElement ($parser, $name) { - $id = $curNodeAttrs['id']; + $id = (int) $curNodeAttrs['id']; if ($mode == 'modify') { if (! $deleteTagStmt->execute()) { @@ -140,36 +152,36 @@ function endElement ($parser, $name) { $k = 'lat'; $v = $curNodeAttrs['lat']; - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; $k = 'lon'; $v = $curNodeAttrs['lon']; - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; $k = 'userid'; $v = $curNodeAttrs['user']; - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; $k = 'version'; $v = $curNodeAttrs['version']; - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; $k = 'timestamp'; $v = $curNodeAttrs['timestamp']; - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; foreach($curNodeTags as $k => $v) { - $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, '$k', '$v');\n"; + $sql_statement = $sql_statement . "INSERT INTO tag (id, k, v) VALUES ($id, " . sqlString($k) . ", " . sqlString($v) . ");\n"; } } else if ($mode == 'modify') { // delete former surveillance nodes that were modified to non-surveillance nodes - $id = $curNodeAttrs['id']; + $id = (int) $curNodeAttrs['id']; if (! $deleteTagStmt->execute()) { echo "***** Error : Deleting tags $id for non-surveillance node : ". $deleteTagStmt->error . "\n"; @@ -208,11 +220,13 @@ while ($data = fread($fp, 4096)) { } } -$sql="UPDATE sync_state SET v='" . $_SERVER['argv'][1] ."' WHERE k='sequenceNumber';"; -$mysqli->query($sql); -$sql_statement = $sql_statement . "UPDATE sync_state SET v='" . $_SERVER['argv'][1] ."' WHERE k='sequenceNumber';\n"; +$syncStmt = $mysqli->prepare("UPDATE sync_state SET v=? WHERE k='sequenceNumber'"); +$syncStmt->bind_param('s', $seqNum); +$syncStmt->execute(); +$syncStmt->close(); +$sql_statement = $sql_statement . "UPDATE sync_state SET v='" . $seqNum ."' WHERE k='sequenceNumber';\n"; $sql_statement = $sql_statement . "COMMIT;"; -$sql_file = "/migrations/".$_SERVER['argv'][1].".sql"; +$sql_file = "/migrations/".$seqNum.".sql"; file_put_contents($sql_file, $sql_statement); $mysqli->close(); diff --git a/data_handler/utils/update_camera.php b/data_handler/utils/update_camera.php index dc88e48..539e0ca 100755 --- a/data_handler/utils/update_camera.php +++ b/data_handler/utils/update_camera.php @@ -2,6 +2,12 @@ include "config.php"; +$seqNum = $_SERVER['argv'][1] ?? ''; +if (!ctype_digit($seqNum)) { + echo "Invalid sequence number\n"; + exit(1); +} + $elementTypes = array(); $count = 0; $countDelete = 0; @@ -281,8 +287,10 @@ while ($data = fread($fp, 4096)) { } } -$sql="UPDATE sync_state SET v='" . $_SERVER['argv'][1] ."' WHERE k='sequenceNumber';"; -$mysqli->query($sql); +$syncStmt = $mysqli->prepare("UPDATE sync_state SET v=? WHERE k='sequenceNumber'"); +$syncStmt->bind_param('s', $seqNum); +$syncStmt->execute(); +$syncStmt->close(); $mysqli->commit(); printDebug(); diff --git a/web/Containerfile b/web/Containerfile index e8b5998..c84f8a7 100644 --- a/web/Containerfile +++ b/web/Containerfile @@ -1,4 +1,17 @@ FROM docker.io/library/php:apache RUN docker-php-ext-install mysqli bcmath -COPY ./www/* /var/www/html \ No newline at end of file + +# Use PHP's production defaults (no error output to clients) and hide the PHP version. +RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ + && printf 'expose_php = Off\n' > "$PHP_INI_DIR/conf.d/zz-hardening.ini" + +# Disable directory listings and reduce the server signature. +RUN sed -i 's/^\(\s*Options\) Indexes FollowSymLinks/\1 FollowSymLinks/' /etc/apache2/apache2.conf \ + && sed -i -e 's/^ServerTokens .*/ServerTokens Prod/' -e 's/^ServerSignature .*/ServerSignature Off/' /etc/apache2/conf-available/security.conf + +COPY ./www/* /var/www/html + +# The web server must not be able to create or modify files in the document root. +RUN chown -R root:root /var/www/html \ + && chmod -R u=rwX,go=rX /var/www/html 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 1cbdd46..a2db7a5 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 + ''; } diff --git a/web/www/sunders/js/search.js b/web/www/sunders/js/search.js index 4ba00a0..38f69a5 100644 --- a/web/www/sunders/js/search.js +++ b/web/www/sunders/js/search.js @@ -37,15 +37,32 @@ function searchLocation(maxResults) { searchLimit = 11; } - var searchScript = document.createElement('script'); - searchScript.src = 'https://nominatim.openstreetmap.org/search.php?q=' + searchString + '&format=json&json_callback=parseResult&limit=' + searchLimit; - document.body.appendChild(searchScript); + var searchUrl = 'https://nominatim.openstreetmap.org/search?q=' + encodeURIComponent(searchString) + + '&format=json&limit=' + encodeURIComponent(searchLimit); + fetch(searchUrl) + .then(function(response) { return response.json(); }) + .then(parseResult) + .catch(function() { parseResult([]); }); } } +// Escape a value for use in HTML text and in double-quoted attributes. +function escapeSearchHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function parseResult(response) { var searchResultDivs = ''; + if (!Array.isArray(response)) { + response = []; + } + if (response.length == searchLimit) { var lastResultIndex = searchLimit - 1; var nextSearchLimit = searchLimit + 10; @@ -67,10 +84,15 @@ function parseResult(response) { // Show the current 10 search results. for (var i=searchLimit-11; i' + - '
' + searchResult.class + ' / ' + searchResult.type + '
' + - '
' + searchResult.display_name + '
' + + '
' + + '
' + escapeSearchHtml(searchResult.class) + ' / ' + escapeSearchHtml(searchResult.type) + '
' + + '
' + escapeSearchHtml(searchResult.display_name) + '
' + '
'; } diff --git a/web/www/sunders/stats/index.php b/web/www/sunders/stats/index.php index 7a0dac4..56ef660 100644 --- a/web/www/sunders/stats/index.php +++ b/web/www/sunders/stats/index.php @@ -84,8 +84,8 @@ } else { $i18n = $i18nCountries; $i18nDefault = $i18nCountriesDefault; - // Check if $key consists of 2 capital letters. - $key = preg_match('/\b[A-Z]{2}\b/', $valsArray[$i]) === 1 || '--' ? $valsArray[$i] : 'dontfindme'; + // Check if $key consists of 2 capital letters or is '--' (unknown country). + $key = (preg_match('/^[A-Z]{2}$/', $valsArray[$i]) === 1 || $valsArray[$i] === '--') ? $valsArray[$i] : 'dontfindme'; } if (!isValidValue($i18n, $i18nDefault, $key)) { // ignore 'vals' and 'cols'