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:
Hosteroid
2025-10-10 00:04:12 +03:00
parent 98f37c2482
commit a29becc944
41 changed files with 1689 additions and 77 deletions

View File

@@ -6,18 +6,21 @@ use Core\Controller;
use App\Models\TldRegistry;
use App\Models\TldImportLog;
use App\Services\TldRegistryService;
use App\Services\Logger;
class TldRegistryController extends Controller
{
private TldRegistry $tldModel;
private TldImportLog $importLogModel;
private TldRegistryService $tldService;
private Logger $logger;
public function __construct()
{
$this->tldModel = new TldRegistry();
$this->importLogModel = new TldImportLog();
$this->tldService = new TldRegistryService();
$this->logger = new Logger('tld_registry_controller');
}
/**
@@ -37,7 +40,7 @@ class TldRegistryController extends Controller
*/
public function index()
{
$search = $_GET['search'] ?? '';
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
$status = $_GET['status'] ?? '';
$dataType = $_GET['data_type'] ?? '';
$page = max(1, (int)($_GET['page'] ?? 1));
@@ -95,6 +98,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importTldList();
@@ -130,6 +136,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importRdapData();
@@ -165,6 +174,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importWhoisDataForMissingTlds();
$remainingCount = $this->tldService->getTldsNeedingWhoisCount();
@@ -246,9 +258,20 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$importType = $_POST['import_type'] ?? '';
$this->logger->separator('Start Progressive Import');
$this->logger->info('Import requested', [
'type' => $importType,
'user_id' => $_SESSION['user_id'] ?? 'unknown',
'username' => $_SESSION['username'] ?? 'unknown'
]);
if (!in_array($importType, ['tld_list', 'rdap', 'whois', 'check_updates', 'complete_workflow'])) {
$this->logger->warning('Invalid import type provided', ['type' => $importType]);
$_SESSION['error'] = 'Invalid import type';
$this->redirect('/tld-registry');
return;
@@ -257,15 +280,27 @@ class TldRegistryController extends Controller
try {
$result = $this->tldService->startProgressiveImport($importType);
$this->logger->info('Import started', [
'status' => $result['status'],
'log_id' => $result['log_id'] ?? null,
'message' => $result['message'] ?? ''
]);
if ($result['status'] === 'complete') {
$_SESSION['success'] = $result['message'];
$this->redirect('/tld-registry');
} else {
// Redirect to progress page
$this->logger->info('Redirecting to progress page', ['log_id' => $result['log_id']]);
$this->redirect('/tld-registry/import-progress/' . $result['log_id']);
}
} catch (\Exception $e) {
$this->logger->error('Failed to start import', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
$_SESSION['error'] = 'Failed to start import: ' . $e->getMessage();
$this->redirect('/tld-registry');
}
@@ -278,7 +313,10 @@ class TldRegistryController extends Controller
{
$logId = $params['log_id'] ?? 0;
$this->logger->info('Import progress page requested', ['log_id' => $logId]);
if (!$logId) {
$this->logger->warning('Progress page requested with no log_id');
$_SESSION['error'] = 'Invalid import session';
$this->redirect('/tld-registry');
return;
@@ -287,17 +325,25 @@ class TldRegistryController extends Controller
// Get import type from log
$log = $this->importLogModel->find($logId);
if (!$log) {
$this->logger->error('Import log not found', ['log_id' => $logId]);
$_SESSION['error'] = 'Import log not found';
$this->redirect('/tld-registry');
return;
}
$importType = $log['import_type'];
$this->logger->info('Showing progress page', [
'log_id' => $logId,
'import_type' => $importType,
'status' => $log['status']
]);
$titles = [
'tld_list' => 'TLD List Import Progress',
'rdap' => 'RDAP Import Progress',
'whois' => 'WHOIS Import Progress',
'check_updates' => 'Update Check Progress'
'check_updates' => 'Update Check Progress',
'complete_workflow' => 'Complete TLD Import Workflow'
];
$this->view('tld-registry/import-progress', [
@@ -312,20 +358,104 @@ class TldRegistryController extends Controller
*/
public function apiGetImportProgress()
{
$logId = $_GET['log_id'] ?? 0;
// Start detailed logging
$this->logger->separator('API Import Progress Request');
$this->logger->info('API called', [
'log_id' => $_GET['log_id'] ?? 'none',
'user_id' => $_SESSION['user_id'] ?? 'not set',
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
]);
// Start output buffering to catch any accidental output
ob_start();
if (!$logId) {
http_response_code(400);
echo json_encode(['error' => 'Log ID required']);
return;
}
try {
$result = $this->tldService->processNextBatch($logId);
echo json_encode($result);
} catch (\Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
// Clear any previous output
ob_clean();
// Set JSON header immediately
header('Content-Type: application/json');
$logId = $_GET['log_id'] ?? 0;
if (!$logId) {
$this->logger->warning('API call with missing log_id');
ob_end_clean();
$this->json(['error' => 'Log ID required'], 400);
return;
}
// Ensure user is authenticated
if (!isset($_SESSION['user_id'])) {
$this->logger->warning('Unauthenticated API call attempt', ['log_id' => $logId]);
ob_end_clean();
$this->json(['error' => 'Authentication required'], 401);
return;
}
$this->logger->info('Processing batch', ['log_id' => $logId]);
// Add detailed logging around the service call
$this->logger->info('About to call TldRegistryService::processNextBatch', [
'log_id' => $logId,
'service_class' => get_class($this->tldService)
]);
try {
$result = $this->tldService->processNextBatch($logId);
$this->logger->info('processNextBatch returned successfully');
} catch (\Throwable $e) {
$this->logger->critical('processNextBatch threw exception', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
throw $e;
}
$this->logger->info('Batch processing result', [
'status' => $result['status'] ?? 'unknown',
'processed' => $result['processed'] ?? 0,
'remaining' => $result['remaining'] ?? 0
]);
// Clean output buffer before sending JSON
ob_end_clean();
$this->json($result);
} catch (\Throwable $e) {
// Catch ALL errors including fatal errors
// Clean any buffered output
if (ob_get_level() > 0) {
ob_end_clean();
}
// Detailed error logging
$this->logger->critical('API Import Progress Fatal Error', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'log_id' => $_GET['log_id'] ?? 'none'
]);
$this->logger->error('Stack trace', ['trace' => $e->getTraceAsString()]);
$this->logger->separator('API Error End');
// Ensure we always send JSON
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json');
}
echo json_encode([
'error' => 'An error occurred while processing the import',
'message' => $e->getMessage(),
'status' => 'error',
'log_id' => $_GET['log_id'] ?? null,
'error_type' => get_class($e)
]);
exit;
}
}
@@ -341,6 +471,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$tldIds = $_POST['tld_ids'] ?? [];
if (empty($tldIds)) {
@@ -349,6 +482,14 @@ class TldRegistryController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($tldIds, 500, 'TLD selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/tld-registry');
return;
}
try {
$deletedCount = 0;
foreach ($tldIds as $id) {
@@ -468,8 +609,7 @@ class TldRegistryController extends Controller
$domain = $_GET['domain'] ?? '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'Domain parameter is required']);
$this->json(['error' => 'Domain parameter is required'], 400);
return;
}
@@ -477,23 +617,22 @@ class TldRegistryController extends Controller
$tldInfo = $this->tldService->getTldInfo($domain);
if ($tldInfo) {
echo json_encode([
$this->json([
'success' => true,
'data' => $tldInfo
]);
} else {
echo json_encode([
$this->json([
'success' => false,
'message' => 'TLD information not found'
]);
}
} catch (\Exception $e) {
http_response_code(500);
echo json_encode([
$this->json([
'success' => false,
'error' => $e->getMessage()
]);
], 500);
}
}