UI fixes, code tightening, security fixes

This commit is contained in:
Andy Dixon
2026-03-17 10:19:21 +00:00
parent 40cc6b8c66
commit 42fe5b9304
9 changed files with 335 additions and 78 deletions
+69 -3
View File
@@ -4,14 +4,38 @@ namespace NeoCMS;
class Authentication
{
private $credentials;
private array $credentials;
public function __construct(array $credentials)
{
session_start();
$this->startSession();
$this->credentials = $credentials;
}
private function startSession(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
session_set_cookie_params([
'httponly' => true,
'path' => '/',
'samesite' => 'Lax',
'secure' => $isHttps,
]);
session_start();
}
/**
* Logs a user into the CMS
* @param $username
@@ -20,9 +44,11 @@ class Authentication
*/
public function login($username, $password)
{
if (!empty($this->credentials[$username]) && $this->credentials[$username] === $password) {
if ($this->credentialsAreValid((string) $username, (string) $password)) {
session_regenerate_id(true);
$_SESSION['loggedIn'] = true;
$_SESSION['loggedInUser'] = $username;
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
return true;
}
@@ -48,12 +74,52 @@ class Authentication
return $_SESSION['loggedInUser'] ?? 'Not logged in';
}
public function getCsrfToken(): string
{
if (empty($_SESSION['csrfToken'])) {
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrfToken'];
}
public function isValidCsrfToken(?string $token): bool
{
if (!is_string($token) || $token === '') {
return false;
}
return hash_equals($this->getCsrfToken(), $token);
}
/**
* Destroy the session
* @return void
*/
public function logout()
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
}
private function credentialsAreValid(string $username, string $password): bool
{
$storedCredential = $this->credentials[$username] ?? null;
if (!is_string($storedCredential) || $storedCredential === '') {
return false;
}
$passwordInfo = password_get_info($storedCredential);
if (($passwordInfo['algo'] ?? null) !== null) {
return password_verify($password, $storedCredential);
}
return hash_equals($storedCredential, $password);
}
}
+19 -4
View File
@@ -34,7 +34,7 @@ class CMSController
}
// Check if the user is authenticated for actions that require it
if (in_array($action, ['save', 'newPage']) && !$this->authentication->isLoggedIn()) {
if (!$this->authentication->isLoggedIn()) {
$this->sendJsonResponse(['error' => 'Not authenticated']);
return;
}
@@ -44,7 +44,11 @@ class CMSController
try {
$this->$methodName();
} catch (\Exception $e) {
$this->sendJsonResponse(['error' => $e->getMessage()]);
$this->logger->write(
"Controller action {$action} failed: {$e->getMessage()}",
$this->authentication->getLoggedInUser()
);
$this->sendJsonResponse(['error' => 'Request failed']);
}
} else {
$this->sendJsonResponse(['error' => 'Unknown action']);
@@ -59,10 +63,11 @@ class CMSController
private function saveAction(): void
{
$this->ensurePostRequest();
$this->ensureValidCsrfToken();
$uri = $_POST['uri'] ?? '';
$content = $_POST['content'] ?? '';
if (empty($uri) || empty($content)) {
if ($uri === '' || !is_string($content)) {
$this->sendJsonResponse(['error' => 'URI and content are required']);
return;
}
@@ -101,6 +106,7 @@ class CMSController
private function newPageAction(): void
{
$this->ensurePostRequest();
$this->ensureValidCsrfToken();
$filename = $_POST['filename'] ?? '';
$template = $_POST['template'] ?? '';
@@ -275,6 +281,14 @@ class CMSController
}
}
private function ensureValidCsrfToken(): void
{
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (!$this->authentication->isValidCsrfToken($token)) {
throw new \Exception('Invalid CSRF token');
}
}
/**
* Send the JSON response
* @param $data
@@ -283,6 +297,7 @@ class CMSController
private function sendJsonResponse($data): void
{
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
echo json_encode($data);
}
@@ -307,7 +322,7 @@ class CMSController
// Find all elements with the class "editable"
$xpath = new \DOMXPath($dom);
$editableElements = $xpath->query('//*[contains(@class, "editable")]');
$editableElements = $xpath->query('//*[contains(concat(" ", normalize-space(@class), " "), " editable ")]');
// Return true if there are any elements with the "editable" class
return $editableElements->length > 0;
+8 -2
View File
@@ -37,10 +37,16 @@ class Logger
$entry = sprintf(
"[%s]\tUser: %s\t%s\n",
date('Y-m-d H:i:s'),
$user,
trim($message)
$this->normaliseLogField($user),
$this->normaliseLogField($message)
);
file_put_contents($logFile, $entry, FILE_APPEND);
}
private function normaliseLogField($value): string
{
$value = trim((string) $value);
return str_replace(["\r", "\n", "\t"], [' ', ' ', ' '], $value);
}
}