Correggi bug di sicurezza e integrita' dei dati

I due piu' gravi potevano causare perdita di dati silenziosa.

AppServiceProvider chiamava key:generate --force a ogni richiesta quando
APP_KEY risultava vuota. Poiche' nome e cognome dei proclamatori sono
cifrati con quella chiave, ogni riga scritta sotto una chiave poi
sostituita diventava irrecuperabile. La chiave e' ora risolta una sola
volta all'avvio del container e il provider solleva un'eccezione se manca,
invece di rigenerarla.

importTerritoryPdfFolder() caricava i PDF, costruiva l'elenco dei file e
poi lo scartava senza mai accodare il job: nessun errore, nessun territorio
aggiornato, e i file restavano su disco a crescita illimitata perche' la
pulizia viveva solo nel finally del job mai eseguito.

Sicurezza:
- i permessi non venivano riapplicati alle richieste /livewire/update, per
  cui un permesso revocato restava attivo fino al reload della pagina
- host header injection: nginx passava l'Host del client come
  X-Forwarded-Host e Laravel si fidava di tutti i proxy, abbastanza per far
  generare a route() link verso domini arbitrari, inclusi i PDF pubblici
  che contengono un codice di accesso valido
- lo schema forwarded e' ora deciso dal server e non dall'header del client
- campagna_id in Rientra e' #[Locked]: era derivato lato server ma
  manipolabile dal client, permettendo di conteggiare un rientro su una
  campagna arbitraria
- sortField del registro passava non validato a orderBy(), con 500 su
  colonna inesistente
- SESSION_SECURE_COOKIE ora vale true fuori da local
- limiti su dimensione e numero di voci negli import ZIP

Integrita' e prestazioni:
- il default di assignment_link_ttl_hours era rimasto 24 dopo la
  conversione da ore a mesi: i link PDF pubblici duravano due anni invece
  di uno mese
- il seeder usava syncPermissions(), che a un secondo giro azzerava i
  permessi personalizzati dei ruoli; ora e' additivo
- pdftoppm senza timeout poteva bloccare il worker indefinitamente
- nomi file deterministici nell'import: "1 A" e "1-A" scrivevano sullo
  stesso PDF, sovrascrivendosi
- Setting::flushCache() dopo le cancellazioni massive, che non emettono
  eventi e lasciavano la cache puntata a una riga inesistente
- riga settings creata dal seeder, per evitare due singleton concorrenti
- rimosse due migrazioni ridondanti che rompevano migrate:rollback
- indici su assigned_at, (attivo, deleted_at) e deleted_at
- risolti gli N+1 negli elenchi territori e proclamatori

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 16:49:42 +02:00
parent cf78550516
commit 0502fad996
19 changed files with 347 additions and 93 deletions

View File

@@ -4,8 +4,9 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\URL;
use Livewire\Livewire;
use RuntimeException;
class AppServiceProvider extends ServiceProvider
{
@@ -16,16 +17,57 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
// Force HTTPS when behind a reverse proxy
if (str_starts_with(config('app.url'), 'https://')) {
URL::forceScheme('https');
// Pin generated URLs to APP_URL.
//
// Behind a trusted proxy, Symfony derives the port from
// X-Forwarded-Port and falls back to the scheme's default when that
// header is absent — which silently drops a non-standard port from
// every generated link. Anchoring the root to APP_URL makes the
// public address explicit instead of reconstructed per request, and
// keeps PDF/e-mail links correct regardless of proxy behaviour.
$appUrl = (string) config('app.url');
if ($appUrl !== '') {
URL::forceRootUrl($appUrl);
if (str_starts_with($appUrl, 'https://')) {
URL::forceScheme('https');
}
}
// Auto-generate APP_KEY if missing
if (empty(config('app.key'))) {
Artisan::call('key:generate', ['--force' => true]);
// NOTE: APP_KEY is deliberately NOT auto-generated here.
//
// This provider boots on every request. Generating a key at runtime
// meant that if the key ever failed to persist, each request could
// mint a *different* one — and since proclamatori names are encrypted
// at column level with APP_KEY, every row written under a superseded
// key becomes permanently unreadable.
//
// The key is now resolved once at container start (docker/php/
// entrypoint.sh) and persisted to the storage volume. Fail loudly
// instead of silently rotating.
// Only enforced when serving HTTP. Build-time tooling (package:discover,
// config:cache) legitimately runs before the key exists, and failing
// there would break the image build rather than surface a real problem.
if (empty(config('app.key')) && ! $this->app->runningInConsole()) {
throw new RuntimeException(
'APP_KEY is not set. Set it in the environment before starting the application. '
.'Generate one with: php artisan key:generate --show'
);
}
// Livewire only re-applies a fixed allow-list of middleware to
// /livewire/update requests. Spatie's permission middleware is not on
// that list, so a component whose route is guarded by
// `permission:territori.manage` would keep accepting mutating actions
// from a user whose permission was revoked after page load, until they
// reload. Registering them as persistent closes that window.
Livewire::addPersistentMiddleware([
\Spatie\Permission\Middleware\PermissionMiddleware::class,
\Spatie\Permission\Middleware\RoleMiddleware::class,
\Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
Gate::before(function ($user, $ability) {
return $user->hasRole('amministratore') ? true : null;
});