Harden Project 01 public endpoints
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
$tests = [
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'test_layer_panes.php',
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'test_public_hardening_static.php',
|
||||
];
|
||||
|
||||
foreach ($tests as $test) {
|
||||
passthru(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($test), $exitCode);
|
||||
if ($exitCode !== 0) {
|
||||
exit($exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
echo "PASS: all Project 01 tests passed\n";
|
||||
@@ -32,9 +32,14 @@ foreach (['choroplethPane', 'parsilPane', 'jalanPane', 'drawPane', 'pointPane']
|
||||
assert_contains($index, 'map.createPane(name)', 'index.php');
|
||||
assert_contains($index, '.leaflet-pane.is-hit-disabled *', 'choropleth disabled pane CSS');
|
||||
assert_contains($index, 'function setDrawBlockingLayerHitTesting(enabled)', 'draw blocking pane helper');
|
||||
foreach (['choroplethPane', 'parsilPane', 'jalanPane', 'pointPane', 'drawPane'] as $pane) {
|
||||
assert_contains($index, "['choroplethPane', 'parsilPane', 'jalanPane', 'pointPane'].forEach", 'draw blocking pane list excludes drawPane');
|
||||
foreach (['choroplethPane', 'parsilPane', 'jalanPane', 'pointPane'] as $pane) {
|
||||
assert_contains($index, "'{$pane}'", 'draw blocking pane list');
|
||||
}
|
||||
assert_contains($index, 'function syncModeGlobals()', 'mode globals sync helper');
|
||||
assert_contains($index, 'window.currentMode = currentMode;', 'currentMode global sync');
|
||||
assert_contains($index, 'window.currentSubMode = currentSubMode;', 'currentSubMode global sync');
|
||||
assert_contains($index, 'window.activeTool = activeTool;', 'activeTool global sync');
|
||||
assert_contains($index, 'function setChoroplethHitTesting(enabled)', 'choropleth hit-testing helper');
|
||||
assert_contains($index, "pane.style.pointerEvents = enabled ? '' : 'none';", 'choropleth hit-testing toggle');
|
||||
assert_contains($index, "pane.classList.toggle('is-hit-disabled', !enabled);", 'choropleth disabled pane class toggle');
|
||||
@@ -48,6 +53,12 @@ assert_contains($index, 'onclick="cancelActiveDrawing()"', 'draw hint cancel but
|
||||
assert_contains($choropleth, "pane: 'choroplethPane'", 'choropleth layer options');
|
||||
assert_contains($choropleth, 'function isClassicDrawingActive()', 'choropleth draw guard');
|
||||
assert_contains($choropleth, 'if (isClassicDrawingActive()) return;', 'choropleth draw guard usage');
|
||||
assert_contains($choropleth, "window.activeTool === 'jalan'", 'choropleth active tool draw fallback');
|
||||
assert_contains($choropleth, "document.getElementById('drawHint')", 'choropleth draw hint fallback');
|
||||
assert_contains($choropleth, 'function forwardDrawingEventToMap(type, e)', 'choropleth drawing event forwarding helper');
|
||||
assert_contains($choropleth, "if (forwardDrawingEventToMap('click', e)) return;", 'choropleth forwards drawing clicks to map');
|
||||
assert_contains($choropleth, "layer.on('dblclick'", 'choropleth double click handler');
|
||||
assert_contains($choropleth, "if (forwardDrawingEventToMap('dblclick', e)) return;", 'choropleth forwards drawing double clicks to map');
|
||||
assert_contains($choropleth, 'function setLayerHitTesting(leafletLayer, enabled)', 'choropleth layer hit-testing helper');
|
||||
assert_contains($choropleth, "layer._path.style.pointerEvents = enabled ? '' : 'none';", 'choropleth path hit-testing toggle');
|
||||
assert_contains($choropleth, 'setHitTesting: setAllHitTesting', 'choropleth public hit-testing API');
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
|
||||
function read_required(string $relative): string
|
||||
{
|
||||
global $root;
|
||||
$path = $root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative);
|
||||
if (!is_file($path)) {
|
||||
fwrite(STDERR, "Missing file: {$relative}\n");
|
||||
exit(1);
|
||||
}
|
||||
return file_get_contents($path);
|
||||
}
|
||||
|
||||
function assert_contains_text(string $haystack, string $needle, string $label): void
|
||||
{
|
||||
if (strpos($haystack, $needle) === false) {
|
||||
fwrite(STDERR, "FAIL: {$label} must contain {$needle}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function assert_not_contains_text(string $haystack, string $needle, string $label): void
|
||||
{
|
||||
if (strpos($haystack, $needle) !== false) {
|
||||
fwrite(STDERR, "FAIL: {$label} must not contain {$needle}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$helper = read_required('auth/helper.php');
|
||||
assert_contains_text($helper, 'function require_admin_post', 'auth helper admin POST guard');
|
||||
assert_contains_text($helper, 'function csrf_token', 'auth helper CSRF token');
|
||||
assert_contains_text($helper, 'function verify_csrf_token', 'auth helper CSRF verification');
|
||||
assert_contains_text($helper, "'secure' => is_https()", 'session cookie secure when HTTPS');
|
||||
assert_contains_text($helper, 'session_regenerate_id(true)', 'login/session fixation protection helper');
|
||||
assert_contains_text($helper, 'SESSION_IDLE_TIMEOUT', 'idle timeout constant');
|
||||
assert_contains_text($helper, 'LOGIN_MAX_ATTEMPTS', 'login throttling constant');
|
||||
assert_contains_text($helper, 'function login_throttle_key', 'login throttling key helper');
|
||||
|
||||
$validation = read_required('includes/validation.php');
|
||||
assert_contains_text($validation, 'function validate_lat_lng', 'coordinate validation helper');
|
||||
assert_contains_text($validation, 'function validate_geojson_geometry', 'GeoJSON validation helper');
|
||||
|
||||
$connection = read_required('koneksi.php');
|
||||
assert_not_contains_text($connection, '$conn->connect_error]', 'connection error should not leak raw details');
|
||||
assert_contains_text($connection, 'error_log', 'connection errors are logged server-side');
|
||||
|
||||
foreach ([
|
||||
'api/point/simpan.php',
|
||||
'api/point/hapus.php',
|
||||
'api/point/update_posisi.php',
|
||||
'api/jalan/simpan.php',
|
||||
'api/jalan/hapus.php',
|
||||
'api/parsil/simpan.php',
|
||||
'api/parsil/hapus.php',
|
||||
'api/choropleth/upload.php',
|
||||
'api/choropleth/toggle.php',
|
||||
'api/choropleth/hapus.php',
|
||||
] as $endpoint) {
|
||||
$source = read_required($endpoint);
|
||||
assert_contains_text($source, 'require_admin_post();', "{$endpoint} mutation guard");
|
||||
}
|
||||
|
||||
foreach (['simpan.php', 'hapus.php', 'update_posisi.php'] as $legacy) {
|
||||
$source = read_required($legacy);
|
||||
assert_contains_text($source, 'http_response_code(410);', "{$legacy} disabled status");
|
||||
}
|
||||
|
||||
$index = read_required('index.php');
|
||||
assert_contains_text($index, 'window._CSRF_TOKEN', 'index exposes CSRF token');
|
||||
|
||||
foreach ([
|
||||
'modules/point.js',
|
||||
'modules/jalan.js',
|
||||
'modules/parsil.js',
|
||||
'modules/choropleth.js',
|
||||
] as $module) {
|
||||
$source = read_required($module);
|
||||
assert_contains_text($source, "fd.append('csrf_token'", "{$module} sends CSRF token");
|
||||
}
|
||||
|
||||
$pointModule = read_required('modules/point.js');
|
||||
assert_contains_text($pointModule, 'draggable: window._IS_ADMIN', 'POI marker draggable only for admin');
|
||||
assert_contains_text($pointModule, 'window._IS_ADMIN ? `<button class="btn-hapus"', 'POI delete button admin-only');
|
||||
|
||||
$jalanModule = read_required('modules/jalan.js');
|
||||
assert_contains_text($jalanModule, 'window._IS_ADMIN ? `<button class="btn-hapus"', 'road delete button admin-only');
|
||||
|
||||
$parsilModule = read_required('modules/parsil.js');
|
||||
assert_contains_text($parsilModule, 'window._IS_ADMIN ? `<button class="btn-hapus"', 'parcel delete button admin-only');
|
||||
|
||||
$choroplethModule = read_required('modules/choropleth.js');
|
||||
assert_not_contains_text($choroplethModule, 'deleteLayer(${m.id},\'${safeNama}\')', 'choropleth avoids unsafe inline layer name');
|
||||
|
||||
$htaccess = read_required('.htaccess');
|
||||
assert_contains_text($htaccess, 'setup_database\.sql', 'SQL setup file blocked');
|
||||
assert_contains_text($htaccess, 'tests', 'tests directory blocked');
|
||||
assert_contains_text($htaccess, 'Header always set X-Frame-Options', 'security headers configured');
|
||||
|
||||
$apache = read_required('../docker/apache-webgis.conf');
|
||||
assert_contains_text($apache, '<FilesMatch', 'Apache file blocking configured');
|
||||
assert_contains_text($apache, 'Header always set X-Content-Type-Options', 'Apache security headers configured');
|
||||
|
||||
$schema = read_required('setup_database.sql');
|
||||
assert_not_contains_text($schema, 'Default: admin / password', 'schema must not advertise public default password');
|
||||
|
||||
$connection = read_required('koneksi.php');
|
||||
assert_contains_text($connection, "getenv('APP_DB_USER')", 'connection supports app DB user env');
|
||||
|
||||
echo "PASS: public hardening static guards are configured\n";
|
||||
Reference in New Issue
Block a user