From d88e46b71383e9e6388bc80302dbac4f1234f2c7 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 9 Oct 2026 09:41:50 +1100 Subject: [PATCH] FE: Hardening --- front/php/server/util.php | 26 ++++++++++++++++++-------- front/php/templates/security.php | 29 +++++++++++++++++++++++++++++ front/settings.php | 1 + 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/front/php/server/util.php b/front/php/server/util.php index 989abc23a..cfaa12bdb 100755 --- a/front/php/server/util.php +++ b/front/php/server/util.php @@ -16,17 +16,26 @@ require dirname(__FILE__).'/../templates/skinUI.php'; // check if authenticated require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php'; -$FUNCTION = []; -$SETTINGS = []; +$FUNCTION = ''; +$SETTINGS = ''; + +// savesettings is state-changing and must never be reachable via GET - a GET +// URL can be triggered cross-site by a simple top-level navigation, which +// still carries the session cookie under default SameSite=Lax behavior. +if (($_GET['function'] ?? '') === 'savesettings') { + http_response_code(405); + header('Allow: POST'); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Method not allowed']); + exit(); +} // init request params -if(array_key_exists('function', $_REQUEST) != FALSE) -{ - $FUNCTION = $_REQUEST['function']; +if (array_key_exists('function', $_POST)) { + $FUNCTION = $_POST['function']; } -if(array_key_exists('settings', $_REQUEST) != FALSE) -{ - $SETTINGS = $_REQUEST['settings']; +if (array_key_exists('settings', $_POST)) { + $SETTINGS = $_POST['settings']; } @@ -35,6 +44,7 @@ switch ($FUNCTION) { case 'savesettings': + requireCsrfToken(); saveSettings(); break; diff --git a/front/php/templates/security.php b/front/php/templates/security.php index 605ac7cd3..af30651dc 100755 --- a/front/php/templates/security.php +++ b/front/php/templates/security.php @@ -5,6 +5,35 @@ if (session_status() == PHP_SESSION_NONE) { session_start(); } +/** + * Return this session's CSRF token, generating one on first use. + */ +function getCsrfToken(): string { + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + return $_SESSION['csrf_token']; +} + +/** + * Halt the request with a 403 JSON response unless $_POST['csrf_token'] + * matches this session's token. Guards every state-changing PHP endpoint + * that relies on the ambient session cookie rather than a request body + * credential - without this, a cross-site POST can reuse a logged-in + * operator's session to trigger the same action (CWE-352). + */ +function requireCsrfToken(): void { + $submittedToken = $_POST['csrf_token'] ?? ''; + + if (!is_string($submittedToken) || !hash_equals(getCsrfToken(), $submittedToken)) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); + exit(); + } +} + // Constants $configFolderPath = rtrim(getenv('NETALERTX_CONFIG') ?: '/data/config', '/'); $legacyConfigPath = $_SERVER['DOCUMENT_ROOT'] . "/../config/app.conf"; diff --git a/front/settings.php b/front/settings.php index cd99c0a7c..0e2b88539 100755 --- a/front/settings.php +++ b/front/settings.php @@ -620,6 +620,7 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX url: "php/server/util.php", data: { function: 'savesettings', + csrf_token: , settings: JSON.stringify(settingsArray) }, success: function(data, textStatus) {