KindaRails2Shell (CVE-2026-66066): la patch non annulla il furto del secret_key_base
CVE-2026-66066 espone i segreti di Rails tramite l'upload di un'immagine. La patch chiude il loader, ma non ruota le chiavi e le prove forensi hanno un timer di cancellazione.
Il 29 luglio 2026 il core team di Rails ha rilasciato le versioni 7.2.3.2, 8.0.5.1 e 8.1.3.1 per correggere CVE-2026-66066, CVSS 4.0 pari a 9.5, battezzata KindaRails2Shell dai ricercatori di Ethiack. La patch è piccola e funziona: dice a libvips di rifiutare i loader di formato che non erano mai stati pensati per leggere input ostile. Quello che la patch non può fare è dirti se, in qualche momento delle ultime release di Rails, uno sconosciuto non autenticato abbia caricato un JPEG che JPEG non era e se ne sia andato con il tuo secret_key_base. Quella domanda ha una risposta diversa, una scadenza diversa, e una traccia probatoria che i tuoi stessi background job stanno cancellando adesso.
Cosa fa davvero CVE-2026-66066
Active Storage permette al browser di caricare direttamente sull'object storage e poi di comunicare a Rails cosa ha appena caricato. Rails registra quella dichiarazione nella colonna content_type del blob. Quando più tardi qualcosa richiede una variante — una miniatura, un avatar ridimensionato — Rails consulta quella colonna, decide che il file è un'immagine da processare e passa i byte a libvips.
libvips della colonna non si fida. Annusa i magic byte e sceglie il loader in base a ciò che il file è realmente. Due componenti, due opinioni diverse sullo stesso file, e nessuno che le riconcili. Quel disallineamento è tutto il bug.
libvips include decine di loader di formato appoggiati a librerie di terze parti, e alcuni li marca come unfuzzed: una dichiarazione esplicita del manutentore che quel percorso di codice non è stato irrobustito contro input ostile e non va puntato su contenuto non fidato. Dalla versione 8.13 libvips espone un interruttore per bloccarli. Active Storage non l'ha mai attivato. Sono parole dell'advisory: "libvips reads and writes file formats through operations, many backed by third-party libraries. It marks some as unfuzzed (unsafe for untrusted content)." Active Storage non ha disabilitato le operazioni unfuzzed.
L'analisi di Rapid7 documenta la catena usata dai ricercatori. Il file caricato dichiara un content type immagine sull'endpoint di direct upload. Per libvips sembra un file MATLAB level 5, quindi lo prende in carico l'operazione matload. Per libmatio sottostante è invece un MAT 7.3, cioè un contenitore HDF5. HDF5 supporta la External File List: un dataset i cui contenuti risiedono in un altro file su disco, a un percorso che il file stesso indica. libmatio segue il percorso. I byte tornano indietro come valori di pixel nella miniatura generata.
Questa è l'intera primitiva di lettura. L'attaccante carica un'immagine, chiede la thumbnail e legge un file del tuo server dentro i pixel.
L'escalation che non dovrebbe servire spiegare due volte
Una lettura arbitraria di file su una macchina Rails non è un incidente di divulgazione. È un incidente di furto di chiavi, per via di ciò che il processo Rails può leggere:
secret_key_basee la master key inconfig/master.key- tutto il contenuto di
config/credentials.yml.enc, una volta uscita la master key - le credenziali del database
- le chiavi di storage S3 / GCS / Azure
- i token API di terze parti presenti nell'environment del processo
Con secret_key_base un attaccante firma qualunque cosa Rails sia disposto a verificare. Rapid7 osserva che il materiale di firma recuperato basta a forgiare i parametri delle varianti di ImageProcessing 1.x — senza bisogno di un gadget di deserializzazione Marshal — trasformando la lettura in esecuzione di codice. E anche mettendo da parte la RCE, un secret_key_base rubato significa cookie di sessione forgiati e signed GlobalID forgiati: l'attaccante che torna domani si presenta come un utente valido e correttamente firmato, e ogni log di autenticazione gli dà ragione.
Chi ha seguito il furto delle machine key di SharePoint a luglio riconosce la forma. Cambia il prodotto, non il fallimento. Una volta che la chiave di firma è uscita, chiudere la porta da cui è passata è necessario e insufficiente.
È la terza volta che Active Storage si fida del client
Il motore di Zero Hunt mantiene un indice semantico sui corpus storici di CVE, e puntandolo sul meccanismo di KindaRails2Shell i vicini più prossimi non sono stati altri bug di parsing immagini. Sono stati i precedenti di Active Storage stesso sul direct upload:
- CVE-2026-33173 (marzo 2026, CVSS 5.3) —
DirectUploadsControlleraccettava metadata arbitrari dal client, permettendo di impostare flag interni comeidentifiedeanalyzede quindi di saltare del tutto il rilevamento MIME. - CVE-2020-8162 — l'adapter S3 per il direct upload lasciava che il client modificasse il
Content-Lengthdel proprio upload, aggirando i limiti di dimensione.
Lette insieme, CVE-2026-66066 smette di sembrare un incidente di libvips. Il direct upload è un design in cui il client racconta il proprio file al server, e ciascuno di questi bug è la stessa frase con un sostantivo diverso. L'advisory del 2026 classifica la causa radice come CWE-1188, Initialization of a Resource with an Insecure Default: l'etichetta onesta. Nessuno ha scelto libvips. È diventato il variant processor predefinito per qualsiasi applicazione su load_defaults 7.0 o successivo, e le applicazioni oggi esposte sono in larghissima parte quelle i cui sviluppatori non hanno mai preso alcuna decisione sull'image processing.
"Noi non facciamo elaborazione di immagini, lasciamo solo impostare la foto profilo."
Quella frase descrive un'applicazione vulnerabile.
"Sono impattato?" non è una domanda di versione
Gli intervalli dell'advisory sono precisi, e sono solo la prima di tre condizioni:
| Condizione | Come verificarla |
|---|---|
activestorage sotto 7.2.3.2, oppure 8.0.x sotto 8.0.5.1, oppure 8.1.x sotto 8.1.3.1 |
bundle list | grep activestorage |
Variant processor impostato su :vips (default da load_defaults 7.0) |
grep -rn variant_processor config/ — nessun risultato significa che sei sul default |
| Un utente non fidato può far entrare un file in Active Storage e innescare la generazione di una variante | bin/rails routes | grep direct_upload, poi traccia quali upload arrivano a una chiamata variant |
La terza riga è quella a cui nessun dependency scanner risponde. Due applicazioni con lo stesso identico Gemfile.lock possono essere completamente diverse: una monta l'endpoint di direct upload dietro autenticazione e non genera mai varianti da file utente, l'altra ridimensiona gli avatar delle registrazioni anonime. Stesso SBOM, stessa CVE, rischio opposto. Rails 6.x, per contro, non è vulnerabile con la configurazione di default ma lo è se qualcuno ha configurato Vips esplicitamente: un caso che il controllo automatico di versione sbaglia in entrambe le direzioni.
Il security team di Rails è stato diretto sui limiti della propria visibilità, dichiarando a The Hacker News di non avere "no telemetry or reasonable estimate" su quante installazioni vive combinino Active Storage, Vips e upload non fidati. La stima pubblica di Ethiack parla di oltre 500.000 siti potenzialmente esposti. Nessuno, manutentori del framework inclusi, può dirti se il tuo è tra questi. Il percorso va testato.
Le prove hanno un timer di cancellazione
Questa è la parte di KindaRails2Shell che quasi tutti gli articoli saltano, ed è quella con un orologio addosso.
L'attacco lascia un artefatto fisico: il file forgiato è un oggetto reale, depositato nel tuo bucket Active Storage. Rails ha pubblicato un repository forense, rails/rails-forensics-CVE-2026-66066, costruito esattamente su questo. Il detector (lib/crafted_mat_file.rb) ha bisogno di due campi di header presi dai primi 128 byte di un oggetto: per questo bin/kr2s_scan_active_storage_blobs.rb può classificare un candidato con una ranged read, senza scaricare ogni blob che tu abbia mai archiviato. Sopra ci stanno due procedure documentate — kr2s-was-i-vulnerable stabilisce la finestra di esposizione, kr2s-was-i-exploited la setaccia in cerca di file forgiati e ricostruisce quali percorsi sono stati letti.
Tutto questo funziona finché l'artefatto esiste ancora. Rapid7 lo segnala esplicitamente: la pulizia schedulata dei blob non associati può rimuovere le prove. Quasi tutte le applicazioni Rails mature eliminano i blob non associati via cron — è l'igiene raccomandata — e un blob di exploit è per costruzione non associato, perché l'attaccante lo ha caricato senza mai collegarlo a un record. Lo strumento che dimostra che sei stato colpito è in gara con il job che hai configurato per tenere pulito il bucket.
Quindi la prima azione di questo incidente non è bundle update. È: fermare il purge job, poi applicare la patch. Invertire l'ordine può distruggere in modo permanente l'unica prova che distingue "abbiamo corretto una vulnerabilità" da "abbiamo subito una violazione e siamo tenuti per legge a dichiararlo". Va tenuto presente anche il limite dichiarato dal repository: copre Active Storage con varianti tracciate sul processor :vips, e "a clean result from this tooling is strong evidence, not proof." Qualsiasi altro percorso di codice che passi file utente a libvips è fuori dal suo perimetro e dentro il tuo.
Remediation
1. Sono impattato?
Prima di ogni altra cosa, sospendi la pulizia dei blob Active Storage — ActiveStorage::PurgeJob e qualunque sweeper custom dei blob non associati. Questo per primo.
# Versione del framework
bundle list | grep -E 'activestorage|^ \* rails '
# Variant processor: nessun output significa default (:vips) su load_defaults 7.0+
grep -rn "variant_processor" config/
# Versioni di libvips e del binding
vips --version # serve >= 8.13
bundle list | grep ruby-vips # serve >= 2.2.1
# L'endpoint di direct upload è raggiungibile, e da chi?
bin/rails routes | grep direct_upload
Sei impattato quando valgono tutte e tre: activestorage non patchato, processor :vips, e un percorso per cui il file di un utente non fidato arriva alla generazione di una variante. Rails 6.0.0–6.1.7.10 sono impattati solo dove Vips è stato configurato esplicitamente.
2. Patch — versioni corrette esatte
| Branch | Versione corretta |
|---|---|
| Rails 7.0.0 – 7.2.3.1 | 7.2.3.2 |
| Rails 8.0.0 – 8.0.5 | 8.0.5.1 |
| Rails 8.1.0 – 8.1.3 | 8.1.3.1 |
| libvips | ≥ 8.13 |
| ruby-vips | ≥ 2.2.1 |
bundle update rails activestorage
Il requisito su libvips non è opzionale: la patch funziona chiamando l'API di block-untrusted, che le versioni precedenti non espongono. Va messo in conto un cambiamento di comportamento — le applicazioni che generano varianti di BMP, ICO, PSD o formati simili vedranno la generazione rompersi, e dovranno rimuovere quei tipi da variable_content_types. È la correzione che funziona, non una regressione.
3. Non puoi patchare subito? Controlli compensativi
- Imposta
VIPS_BLOCK_UNTRUSTED=1nell'environment applicativo (richiede libvips ≥ 8.13), oppure chiamaVips.block_untrusted(true)da un initializer con ruby-vips ≥ 2.2.1. È lo stesso interruttore che la patch attiva. - Passa a
config.active_storage.variant_processor = :mini_magick. Secondo Ethiack solo il processor Vips è vulnerabile per questo vettore: da trattare come ponte, non come destinazione — ImageMagick ha una sua storia di delegate. - Smetti di fidarti del
content_typedel client: valida i magic byte reali lato server in ingestione e rifiuta i file i cui byte contraddicono la dichiarazione. Questo uccide la classe, non solo la CVE. - Limita l'egress del worker che esegue la generazione delle varianti. Non deve poter raggiungere destinazioni arbitrarie: alza il costo di trasformare una lettura in un canale di esfiltrazione.
4. Caccia alla compromissione
Esegui il tooling ufficiale contro il tuo storage backend, in quest'ordine:
git clone https://github.com/rails/rails-forensics-CVE-2026-66066
# 1) ricostruisci la finestra di esposizione dalla storia dei deploy
# -> skills/kr2s-was-i-vulnerable
# 2) setaccia quella finestra in cerca di blob forgiati
ruby bin/kr2s_scan_active_storage_blobs.rb # self-contained: copialo nel container
Segnali da correlare in parallelo:
- Livello blob: qualunque blob che dichiari un
content_typeimmagine ma i cui primi byte si identifichino come MAT/HDF5. È l'indicatore diretto su cui lavora lo scanner. - Livello richiesta: una
POSTverso/rails/active_storage/direct_uploadsseguita entro pochi secondi dalla richiesta di variante per lo stesso blob, dalla stessa sorgente, ripetutamente. Gli utenti legittimi caricano e poi navigano; non enumerano. - Blob non associati caricati durante la finestra di esposizione e mai collegati ad alcun record.
- Dopo il furto: è qui che un incidente di lettura diventa visibile sul filo. Cookie di sessione validi e correttamente firmati, o signed GlobalID, presentati da un ASN che quell'account non ha mai usato; richieste con parametri firmati di Active Storage o ImageProcessing che la tua applicazione non ha mai generato; il worker delle varianti che apre una connessione in uscita.
Mappatura MITRE ATT&CK della catena: T1190 Exploit Public-Facing Application → T1083 File and Directory Discovery / T1005 Data from Local System → T1552.001 Unsecured Credentials: Credentials In Files → T1550.004 Use Alternate Authentication Material: Web Session Cookie → T1078 Valid Accounts.
5. Bonifica e verifica
Se la scansione trova un blob forgiato — o se la finestra di esposizione è lunga e la retention dei blob è troppo corta per escluderlo — tratta come esposto ogni segreto leggibile dal processo Rails:
secret_key_baseconfig/master.keye l'intero contenuto diconfig/credentials.yml.enc- credenziali del database
- credenziali dei servizi Active Storage (S3, GCS, Azure)
- ogni token API di terze parti nell'environment del processo
L'advisory Rails è netto su un punto che i team sbagliano sotto pressione: "Rotation should only be used as an intermediate step if necessary. Do not retain an exposed secret as a fallback." Rails supporta un fallback di rotazione perché i vecchi cookie continuino a validare durante un deploy: lasciare la chiave compromessa in quella lista significa che continuano a validare anche i cookie forgiati.
Pianifica il raggio d'impatto prima di ruotare, perché cambiare secret_key_base invalida sessioni attive, cookie firmati e cifrati, signed GlobalID e gli URL Active Storage già emessi. Gli utenti rifanno il login; qualunque URL inviato via email smette di risolvere. Fallo comunque.
Poi verifica, in quest'ordine: versione patchata in produzione e libvips ≥ 8.13 confermato nel container in esecuzione; block-untrusted attivo; scansione dei blob rieseguita dopo la patch e pulita; vecchio secret_key_base rimosso da ogni fallback di rotazione; e — il controllo che regge davvero — un cookie di sessione firmato con la vecchia chiave rifiutato in produzione.
Dove si colloca Zero Hunt
Tutto quanto sopra produce un record fattuale, ed è quel record che un regolatore, un assicuratore o un auditor prima o poi chiederà. La domanda scomoda in questo incidente non è "avete patchato". È: qual era la finestra di esposizione, cosa avete scansionato, quando, cosa ha restituito la scansione, e sarete in grado di dimostrarlo fra sei mesi? Con NIS2 l'orologio della notifica parte dalla consapevolezza di un incidente significativo, non dalla sua conferma — e KindaRails2Shell è esattamente il caso in cui la consapevolezza arriva molto prima della conferma, su prove che scadono.
Il layer di compliance di Zero Hunt mappa in continuo ogni finding, scansione e azione di remediation su 32 framework — NIS2 incluso il Titolo 13, GDPR, ISO 27001, SOC 2, DORA con le RTS TLPT — e firma ogni record con ECDSA al momento della scrittura, così la catena di custodia esiste per costruzione invece di essere ricostruita a ritroso dallo scrollback di Slack sotto scadenza. Il Trust Center esporta il bundle con un click. Per una vulnerabilità i cui artefatti forensi hanno un timer di cancellazione, la timeline firmata di cosa hai verificato e quando non è burocrazia: è la differenza tra una posizione difendibile e un'affermazione.
La domanda sull'esposizione trova risposta sul lato offensivo. Lo swarm di 10 agenti di Zero Hunt non confronta il tuo Gemfile.lock con una lista di CVE: un LLM locale scrive una catena costruita sul singolo target — l'agente Recon mappa quali endpoint di upload sono raggiungibili senza credenziali, l'agente Web spinge un upload realmente forgiato attraverso di essi, e l'agente Post-Exploit stabilisce se una richiesta di variante restituisce contenuto di file oppure niente. Ogni skill impiegata viene sottoposta a backtest nell'AI Gym contro Vulhub e il corpus black-box basato su CVE prima di poter avvicinare un target di produzione, e ogni exploit gira in un container effimero sull'appliance, on-premise, senza callback verso il cloud. La risposta che torna non è "stai girando una versione vulnerabile". È "questo endpoint, questo parametro, questo file è stato letto" — oppure un esito pulito su cui puoi mettere la firma.
Se la terza condizione della tabella qui sopra è quella a cui non sai rispondere per il tuo parco applicativo, è quella la conversazione che vale la pena avere.