Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
This commit is contained in:
@@ -29,5 +29,26 @@ abstract class Controller
|
||||
header("Location: $path");
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify CSRF token and redirect with error if invalid
|
||||
*
|
||||
* @param string $redirectUrl URL to redirect to on failure
|
||||
* @return bool True if valid, redirects on failure
|
||||
*/
|
||||
protected function verifyCsrf(string $redirectUrl = '/'): bool
|
||||
{
|
||||
return Csrf::verifyOrFail($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token for forms
|
||||
*
|
||||
* @return string The CSRF token
|
||||
*/
|
||||
protected function getCsrfToken(): string
|
||||
{
|
||||
return Csrf::getToken();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
115
core/Csrf.php
Normal file
115
core/Csrf.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* CSRF Protection
|
||||
*
|
||||
* Provides Cross-Site Request Forgery (CSRF) protection for forms
|
||||
*/
|
||||
class Csrf
|
||||
{
|
||||
private const TOKEN_NAME = 'csrf_token';
|
||||
private const TOKEN_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Generate a new CSRF token and store in session
|
||||
*
|
||||
* @return string The generated token
|
||||
*/
|
||||
public static function generateToken(): string
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(self::TOKEN_LENGTH));
|
||||
$_SESSION[self::TOKEN_NAME] = $token;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current CSRF token (generates if doesn't exist)
|
||||
*
|
||||
* @return string The CSRF token
|
||||
*/
|
||||
public static function getToken(): string
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION[self::TOKEN_NAME])) {
|
||||
return self::generateToken();
|
||||
}
|
||||
|
||||
return $_SESSION[self::TOKEN_NAME];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CSRF token from request
|
||||
*
|
||||
* @param string|null $token Token to validate (from POST/GET)
|
||||
* @return bool True if valid, false otherwise
|
||||
*/
|
||||
public static function validateToken(?string $token): bool
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($_SESSION[self::TOKEN_NAME])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use hash_equals to prevent timing attacks
|
||||
return hash_equals($_SESSION[self::TOKEN_NAME], $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify CSRF token from POST request and redirect with error if invalid
|
||||
*
|
||||
* @param string $redirectUrl URL to redirect to on failure
|
||||
* @return bool True if valid, redirects on failure
|
||||
*/
|
||||
public static function verifyOrFail(string $redirectUrl = '/'): bool
|
||||
{
|
||||
$token = $_POST[self::TOKEN_NAME] ?? '';
|
||||
|
||||
if (!self::validateToken($token)) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION['error'] = 'Security token validation failed. Please try again.';
|
||||
header("Location: $redirectUrl");
|
||||
exit;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML for hidden CSRF token field
|
||||
*
|
||||
* @return string HTML input field
|
||||
*/
|
||||
public static function field(): string
|
||||
{
|
||||
$token = self::getToken();
|
||||
return '<input type="hidden" name="' . self::TOKEN_NAME . '" value="' . htmlspecialchars($token) . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate CSRF token (useful after login/logout)
|
||||
*/
|
||||
public static function regenerateToken(): string
|
||||
{
|
||||
return self::generateToken();
|
||||
}
|
||||
}
|
||||
|
||||
79
core/SessionConfig.php
Normal file
79
core/SessionConfig.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* Session Configuration
|
||||
*
|
||||
* Handles session handler configuration and initialization
|
||||
*/
|
||||
class SessionConfig
|
||||
{
|
||||
/**
|
||||
* Configure and initialize session handler
|
||||
*
|
||||
* Attempts to use database sessions if available, falls back to file sessions
|
||||
*/
|
||||
public static function configure(): void
|
||||
{
|
||||
try {
|
||||
// Check if database sessions are available
|
||||
if (self::isDatabaseSessionsAvailable()) {
|
||||
$sessionLifetime = (int)($_ENV['SESSION_LIFETIME'] ?? 1440);
|
||||
$handler = new DatabaseSessionHandler($sessionLifetime);
|
||||
session_set_save_handler($handler, true);
|
||||
}
|
||||
// If not available, PHP will use default file-based sessions (no action needed)
|
||||
} catch (\Exception $e) {
|
||||
// Fall back to default file-based sessions
|
||||
error_log("Database session handler not available, using file sessions: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database sessions are available
|
||||
*
|
||||
* @return bool True if sessions table exists and database is accessible
|
||||
*/
|
||||
private static function isDatabaseSessionsAvailable(): bool
|
||||
{
|
||||
try {
|
||||
// Check if database credentials are configured
|
||||
if (empty($_ENV['DB_HOST']) || empty($_ENV['DB_DATABASE'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create PDO connection
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_DATABASE']}",
|
||||
$_ENV['DB_USERNAME'],
|
||||
$_ENV['DB_PASSWORD'],
|
||||
[
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
|
||||
]
|
||||
);
|
||||
|
||||
// Check if sessions table exists
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'sessions'");
|
||||
return $stmt->rowCount() > 0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Database not available or sessions table doesn't exist
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start session with validation
|
||||
*/
|
||||
public static function start(): void
|
||||
{
|
||||
session_start();
|
||||
|
||||
// Validate session exists in database (for database-backed sessions)
|
||||
// This ensures deleted sessions are immediately invalidated
|
||||
SessionValidator::validate();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user