[SEC+UX] Hardening sicurezza + miglioramenti UX pre-audit

SICUREZZA:
- index.php: rimosso CORS wildcard in debug mode (solo origini autorizzate)
- AuthController: getClientIP() con X-Forwarded-For sicuro (proxy-aware)
- AuthController: refresh token con SELECT FOR UPDATE in transazione atomica
- AIService: anonimizzazione dati org nei prompt Anthropic API (no nome/fatturato)

UX AUDIT-READY:
- dashboard.html: gauge rinominato 'Avanzamento implementazione misure Art.21'
- incidents.html: decision tree Art.23 con 5 criteri per 'Is Significant?'
- policies.html: banner warning obbligatorio su bozze generate da AI
- risks.html: tooltip dettagliati scala Likelihood/Impact (ISO 27005)
- assessment.html: progress bar % completamento risposta domande

DB:
- migration 006: indici performance + audit_log immutabile (trigger) + soft delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-02-20 12:01:33 +01:00
co-authored by Claude Sonnet 4.6
parent 0e78ec24c1
commit 782389849f
9 changed files with 282 additions and 39 deletions
+47 -18
View File
@@ -10,15 +10,34 @@ require_once APP_PATH . '/services/RateLimitService.php';
class AuthController extends BaseController
{
/**
* Restituisce l'IP reale del client, gestendo proxy/nginx.
*/
private function getClientIP(): string
{
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Fidati di X-Forwarded-For solo se la richiesta arriva da localhost (nginx proxy)
if (in_array($remoteAddr, ['127.0.0.1', '::1', 'unknown'])
&& !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$xForwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'];
$ips = array_map('trim', explode(',', $xForwardedFor));
$firstIp = filter_var($ips[0], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($firstIp !== false) {
return $firstIp;
}
}
return $remoteAddr;
}
/**
* POST /api/auth/register
*/
public function register(): void
{
// Rate limiting
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$ip = $this->getClientIP();
RateLimitService::check("register:{$ip}", RATE_LIMIT_AUTH_REGISTER);
RateLimitService::increment("register:{$ip}");
RateLimitService::increment("register:{$ip}"); // $ip defined above via getClientIP()
$this->validateRequired(['email', 'password', 'full_name']);
@@ -87,7 +106,7 @@ class AuthController extends BaseController
public function login(): void
{
// Rate limiting
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$ip = $this->getClientIP();
RateLimitService::check("login:{$ip}", RATE_LIMIT_AUTH_LOGIN);
RateLimitService::increment("login:{$ip}");
@@ -167,24 +186,34 @@ class AuthController extends BaseController
$refreshToken = $this->getParam('refresh_token');
$hashedToken = hash('sha256', $refreshToken);
// Verifica refresh token
$tokenRecord = Database::fetchOne(
'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW()',
[$hashedToken]
);
// Transazione atomica per evitare race condition (double-spend del refresh token)
Database::beginTransaction();
try {
// SELECT FOR UPDATE: blocca il record per tutta la transazione
$tokenRecord = Database::fetchOne(
'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW() FOR UPDATE',
[$hashedToken]
);
if (!$tokenRecord) {
$this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
if (!$tokenRecord) {
Database::rollback();
$this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
}
// Elimina vecchio token atomicamente
Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);
// Genera nuovi tokens
$userId = (int) $tokenRecord['user_id'];
$accessToken = $this->generateJWT($userId);
$newRefreshToken = $this->generateRefreshToken($userId);
Database::commit();
} catch (Throwable $e) {
Database::rollback();
throw $e;
}
// Elimina vecchio token
Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);
// Genera nuovi tokens
$userId = (int) $tokenRecord['user_id'];
$accessToken = $this->generateJWT($userId);
$newRefreshToken = $this->generateRefreshToken($userId);
$this->jsonSuccess([
'access_token' => $accessToken,
'refresh_token' => $newRefreshToken,