UI fixes, code tightening, security fixes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user