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
| id | ' + (plot.id) + ' |
| user osm | ' + (plot.userid) + ' |
| latitude | ' + (plot.lat) + ' |
| longitude | ' + (plot.lon) + ' |
| id | ' + id + ' |
| user osm | ' + escapeHtml(plot.userid) + ' |
| latitude | ' + escapeHtml(plot.lat) + ' |
| longitude | ' + escapeHtml(plot.lon) + ' |
| ' + x + ' | '; - var descr = plot[x]; - if (descr.substr(0, 4) == 'http') { + popupDataTable = popupDataTable + ' |
| ' + escapeHtml(x) + ' | ';
+ var descr = String(plot[x]);
+ if (/^https?:\/\//i.test(descr)) {
+ var url = escapeHtml(descr);
var suffix = descr.slice(-3).toLowerCase();
if (suffix == 'jpg' || suffix == 'gif' || suffix == 'png') {
- popupDataTable = popupDataTable + ' |