
Problema con tough, versioni precedenti alla 0.20.0 (Multiple CVE)
Nel marzo 2025 sono state divulgate multiple vulnerabilità di sicurezza nella libreria Tough di AWS Labs (un client Rust di TUF – The Update Framework). Questi problemi sono tracciati come CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 e CVE-2025-2888 e sono stati corretti nella versione 0.20.0 di Tough. Gli ingegneri della sicurezza possono utilizzare questo documento per comprendere i dettagli tecnici di ciascuna vulnerabilità, la causa principale nel codice e le patch che le risolvono. A tutti gli utenti di Tough < 0.20.0 è fortemente consigliato di aggiornare alla v0.20.0 o successiva.
Tough non riusciva a convalidare in sequenza la versione dei Root metadata durante l'aggiornamento. Un attaccante che controlla un repository (o con capacità man-in-the-middle) poteva fornire un file di root metadata con un numero di versione inaspettato, portando il client a recuperare e considerare attendibile una versione errata. In sostanza, Tough non garantiva che la versione di un nuovo root metadata fosse esattamente superiore di uno rispetto alla versione precedentemente considerata attendibile, violando il requisito di TUF per una catena continua di fiducia. Ciò poteva portare a attacchi di rollback o mix-and-match, in cui un root vecchio (ma firmato correttamente) viene accettato come se fosse l'ultimo, potenzialmente reintroducendo chiavi di firma ritirate o fiducia scaduta.
tough/src/lib.rs. L'implementazione vulnerabile controllava solo che la versione del nuovo root non fosse inferiore a quella del precedente, invece di richiedere che fosse la versione sequenziale successiva. Questa omissione viola la regola della specifica TUF secondo cui i client devono scaricare le versioni intermedie del root in ordine (senza saltare versioni).Implementazione vulnerabile: Nella versione 0.19.x e precedenti, dopo aver scaricato un nuovo root.json, Tough verificava le firme ma non applicava rigorosamente la continuità delle versioni. Consentiva alla nuova versione del root di avere qualsiasi valore >= la versione considerata attendibile. Ad esempio, se il root attualmente considerato attendibile era la versione N, Tough accettava un nuovo root con versione N+2 o superiore (purché le firme fossero valide), saltando la N+1. Lo snippet di codice seguente illustra il controllo precedente alla correzione:```rust
// (Prior to fix) Allow new root version to be >= old version – too permissive
ensure!(root.signed.version <= new_root.signed.version, error::OlderMetadataSnafu {
role: RoleType::Root,
current_version: root.signed.version,
new_version: new_root.signed.version
});
Un attaccante potrebbe sfruttare questo presentando <b>metadata di root con versione superiore che in realtà sono un vecchio set di chiavi</b>. Tough lo accetterebbe, pensando che sia un aggiornamento, e [si fiderebbe di contenuti firmati con chiavi obsolete](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Implementazione corretta:</b> Il codice corretto (in Tough 0.20.0) richiede esplicitamente che la versione della nuova root sia esattamente uno in più rispetto alla vecchia versione, vedi [commit 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). Inoltre garantisce che la nuova versione sia più alta (impedisce l'uguaglianza o la diminuzione) e previene qualsiasi salto maggiore di +1:```rust
// (Fixed in v0.20.0) Enforce sequential root version update (new_version == old_version + 1)
ensure!(
root.signed.version < new_root.signed.version &&
root.signed.version.get() + 1 == new_root.signed.version.get(),
error::OlderMetadataSnafu {
role: RoleType::Root,
current_version: root.signed.version,
new_version: new_root.signed.version
}
);
Questa modifica garantisce che il client scarichi e applichi i metadati root in ordine (N, N+1, N+2, ...) senza saltare passaggi. Se viene trovato un file di metadati root con una versione diversa da old_version+1, Tough ora lo considera un potenziale attacco e interrompe l'aggiornamento.
Tough gestiva erroneamente i ruoli di destinazione delegati “terminating” come definiti da TUF. In un repository TUF, una delegazione può essere marcata come terminating, il che significa che se una ricerca di target raggiunge quella delegazione e il target non viene trovato, la ricerca deve fermarsi (e non continuare verso altre delegazioni a priorità inferiore). (vedi qui) A causa di un errore logico, Tough non riusciva a terminare la ricerca in questo caso: continuava a cercare nelle delegazioni successive anche quando avrebbe dovuto fermarsi. Questo potrebbe permettere a un attaccante che controlla una delegazione a priorità inferiore di servire contenuti per target che non dovrebbe controllare, bypassando i confini di fiducia previsti.
tough/src/editor/targets.rs (e il relativo codice di risoluzione delle delegazioni). Il codice vulnerabile non gestiva correttamente il flag terminating sulle delegazioni. Iterando attraverso le delegazioni alla ricerca di un target, Tough passava alla delegazione successiva anche se quella corrente era marcata come terminante e non aveva trovato corrispondenza, contrariamente alle specifiche TUF.Implementazione vulnerabile: In Tough <0.20.0, la logica di find_target() semplicemente ricorreva o iterava attraverso tutti i possibili ruoli delegati finché non trovava un target o finché non li esauriva. Non impostava alcun flag né usciva dal ciclo quando incontrava un ruolo terminante. Pseudocodice del vecchio comportamento:```rust
for role in delegation_chain {
if role.has_target(target) {
return target_metadata;
}
// Missing: if role is terminating and target not found, should break.
// Tough erroneously continues to next delegation.
}
Ciò significa che un delegato con priorità inferiore (che dovrebbe essere ignorato dopo una delegazione terminante che lo precede) potrebbe comunque essere consultato e fornire un file di destinazione dannoso
<b>Implementazione corretta:</b> La versione corretta introduce un meccanismo per tracciare la terminazione e interrompere la ricerca in modo appropriato. Quando viene incontrata una delegazione terminante che non contiene il target, Tough ora [esce immediatamente dal ciclo di ricerca](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). Nel codice aggiornato, un flag booleano (ad es. `terminated`) viene impostato quando viene raggiunto un ruolo terminante e propagato lungo lo stack delle chiamate. Ad esempio:```rust
// If a terminating delegation was reached (and we didn't find the target there), stop searching further
if role.terminating && !permissive {
// Mark that we encountered a terminating delegation
*terminated = true;
break;
}
Inoltre, le chiamate ricorsive a find_target ora trasportano un flag terminated per garantire che, una volta segnalata la terminazione, nessun'altra delega venga considerata nei loop di livello superiore. Anche RepositoryEditor::delegate_role di Tough e le strutture correlate sono state aggiornate per memorizzare l'attributo terminating e passarlo attraverso la logica di ricerca.
break su una delega terminante consentiva di prendere in considerazione dati di target non autorizzati.La logica di Tough per rilevare attacchi di rollback nei metadati dello snapshot era incompleta. In particolare, quando si aggiorna il ruolo Snapshot, Tough dovrebbe verificare che tutti i metadati dei target visti in precedenza (inclusi i target delegati) siano ancora presenti e non abbiano una versione arretrata nel nuovo snapshot. (vedi qui) Tough applicava questo controllo per targets.json di primo livello, ma non lo faceva per i file di metadati dei target delegati. Questa lacuna poteva consentire a un attaccante di rimuovere o ripristinare un file di target delegato nei metadati dello snapshot del repository senza essere rilevato, facendo sì che il client accettasse un file di target delegato obsoleto (o mancante) come se fosse aggiornato.
tough/src/lib.rs (funzione che carica/applica i nuovi metadati dello Snapshot). Il codice vulnerabile controllava solo la continuità del ruolo principale targets.json nello snapshot, ma non iterava sui ruoli delegati elencati nei metadati dello snapshot per eseguire controlli simili.Implementazione vulnerabile: In Tough <0.20.0, dopo aver recuperato un nuovo snapshot.json, il client si assicurava che i ruoli root e snapshot non fossero stati sottoposti a rollback e, in particolare, che targets.json fosse ancora presente. Verificava inoltre che la versione di targets.json nel nuovo snapshot fosse >= alla versione precedente. Tuttavia, se lo snapshot conteneva target delegati (ad es., metadati delegati projects.json, user.json), il client non li verificava. Ad esempio, originariamente il codice faceva qualcosa del genere:```rust
// Pseudo-code of original snapshot rollback check (simplified)
if let Some(old_targets_meta) = old_snapshot.meta.get("targets.json") {
let new_targets_meta = new_snapshot.meta.get("targets.json").unwrap();
ensure!(new_targets_meta.version >= old_targets_meta.version, ...);
}
// (No checks for delegated target roles like "projects.json", "user.json", etc.)
Ciò significa che se un attaccante con accesso al repository <b>rimuovesse un file di metadati delegato o lo riportasse a una versione precedente</b>, il client di Tough non se ne accorgerebbe – purché il targets.json primario rimanesse intatto. Il client potrebbe quindi scaricare un target obsoleto da quella delega, senza sapere che avrebbe dovuto essere rifiutato come rollback.
<b>Implementazione corretta:</b> La versione 0.20.0 aggiunge controlli completi per <b>ogni ruolo elencato nei metadati di snapshot</b>. Il nuovo codice scorre ogni voce nei metadati del vecchio snapshot (incluse tutte le deleghe dei ruoli di targets) e garantisce due cose per ciascuna: (1) che il ruolo esista ancora nel nuovo snapshot e (2) che la sua versione non sia diminuita. Se un ruolo manca nel nuovo snapshot o ha un numero di versione inferiore al precedente, l'aggiornamento viene rifiutato come potenziale attacco di rollback. Per esempio:```rust
for (name, old_meta) in &old_snapshot.signed.meta {
// 1. Role must appear in new snapshot
ensure!(
snapshot.signed.meta.contains_key(name),
error::SnapshotRoleMissingSnafu { role: name, old_version: old_snapshot.signed.version, new_version: snapshot.signed.version }
);
// 2. Role’s version must not decrease
let new_meta = snapshot.signed.meta.get(name).unwrap();
ensure!(
old_meta.version <= new_meta.version,
error::SnapshotRoleRollbackSnafu { role: name, old_role_version: old_meta.version, new_role_version: new_meta.version, … }
);
}
Iterando attraverso tutti i ruoli (name rappresenta ogni nome di file di metadati come targets.json, delegated-role.json, ecc.), il client rileverà se alcuni metadati di target delegati sono stati rimossi o ripristinati. Gli errori SnapshotRoleMissing e SnapshotRoleRollback verranno attivati se un ruolo è scomparso o la sua versione è regredita. In particolare, Tough ora garantisce anche esplicitamente che lo snapshot contenga almeno la voce targets.json (altrimenti segnala un errore con SnapshotTargetsMetaMissing) come controllo di coerenza.
Tough gestiva in modo errato un rollback rilevato nel ruolo Timestamp. Il ruolo Timestamp in TUF firma periodicamente la versione più recente dei metadati dello snapshot per aiutare i client a rilevare se stanno vedendo uno snapshot più vecchio (un rollback). Tough eseguiva sì il controllo di rollback sulla versione dello snapshot contenuta nel timestamp, ma solo dopo aver memorizzato nella cache locale i nuovi metadati del timestamp. Se veniva rilevato un rollback, Tough rifiutava l'aggiornamento ma aveva già persistito il timestamp non valido come "più recente" nella sua cache. Ciò significava che un timestamp dannoso (con una versione dello snapshot obsoleta) poteva avvelenare la cache del client. Gli aggiornamenti legittimi successivi potevano quindi apparire a Tough come rollback (poiché la cache conteneva un timestamp che indicava una versione dello snapshot superiore rispetto al nuovo), bloccando aggiornamenti validi.
tough/src/lib.rs (funzione che carica il nuovo timestamp.json). Il problema centrale era un bug nell'ordine delle operazioni: Tough aggiornava la propria cache/stato locale con i nuovi metadati Timestamp prima di convalidare completamente che la versione dello snapshot al loro interno non fosse più vecchia di quella vista in precedenza.Implementazione vulnerabile: In Tough <0.20.0, quando veniva recuperato un nuovo timestamp.json, il client lo analizzava e lo scriveva nella cache (marcandolo come timestamp attendibile corrente) e poi eseguiva il controllo di rollback sulla versione dello snapshot. Se la versione dello snapshot nel nuovo timestamp era inferiore alla versione dello snapshot registrata in precedenza (indicando un tentativo di rollback), Tough registrava un errore e rifiutava quel ciclo di aggiornamento – ma la cache conteneva già il timestamp "cattivo". Non era prevista la rimozione della voce dalla cache in quel percorso di errore. Di conseguenza, la registrazione del timestamp "attendibile" da parte del client poteva diventare quella obsoleta.
Ad esempio, supponiamo che l'ultima versione nota dello snapshot fosse la 5. Un aggressore potrebbe fornire metadati Timestamp (con un numero di versione del timestamp più alto) che firmano la versione 4 dello snapshot. Tough accetterebbe il file timestamp (memorizzandolo nella cache), poi noterebbe che lo snapshot 4 < 5 e uscirebbe dall'aggiornamento con un errore – ma ora la sua cache dice "l'ultimo timestamp indica lo snapshot 4". Quando arriva un timestamp corretto (snapshot 5 o 6), Tough vede lo snapshot 5 rispetto allo snapshot 4 in cache come un altro rollback (poiché si fida erroneamente della versione 4 dello snapshot in cache come riferimento) e quindi rifiuta anche l'aggiornamento valido. Il bollettino AWS descrive questo ciclo: "il client memorizza nella cache i metadati del timestamp nonostante vengano correttamente rifiutati quando viene rilevato un rollback… facendo sì che tough successivamente non riesca a consumare aggiornamenti validi."
Implementazione corretta: La correzione garantisce che i controlli di rollback avvengano prima della memorizzazione nella cache del nuovo timestamp e aggiunge una validazione più rigorosa del contenuto del timestamp. In Tough 0.20.0, la funzione load_timestamp() è stata modificata per imporre che i metadati Timestamp siano ben formati e che la loro versione dello snapshot non sia inferiore alla versione dello snapshot precedentemente attendibile prima di concludere l'aggiornamento. Nello specifico, il codice ora verifica: (a) che i metadati del timestamp abbiano esattamente una voce (deve essere solo per snapshot.json), (b) che quella voce esista e sia analizzata, e (c) che la versione dello snapshot al suo interno sia >= alla versione precedente dello snapshot. (vedi qui) Solo dopo che queste validazioni hanno avuto esito positivo, il nuovo timestamp è considerato attendibile. La logica critica aggiunta è illustrata di seguito:```rust
// Ensure the timestamp meta contains exactly one entry (the snapshot)
ensure!(timestamp.signed.meta.len() == 1, error::TimestampMetaLengthSnafu { … });
let snapshot_meta = timestamp.signed.meta.get("snapshot.json");
ensure!(snapshot_meta.is_some(), error::MissingSnapshotMetaSnafu { … });
// If we have a previously trusted timestamp (old_timestamp): if let Some(old_timestamp) = old_timestamp_opt { // Check that the snapshot version in the new timestamp >= old snapshot version let old_snapshot_meta = old_timestamp.signed.meta.get("snapshot.json").unwrap(); ensure!( old_snapshot_meta.version <= snapshot_meta.unwrap().version, error::OlderSnapshotInTimestampSnafu { // details: new snapshot vs old snapshot versions snapshot_new: snapshot_meta.unwrap().version, snapshot_old: old_snapshot_meta.version, timestamp_new: timestamp.signed.version, timestamp_old: old_timestamp.signed.version } ); }
Con questa modifica, se la versione dello snapshot del nuovo timestamp è inferiore alla versione dello snapshot precedentemente vista, il controllo `ensure!` fallirà <b>prima</b> che il nuovo timestamp venga salvato come attendibile. Viene sollevato l'errore `OlderSnapshotInTimestamp` per interrompere l'aggiornamento. Di conseguenza, Tough manterrà in cache il vecchio timestamp (corretto) quando viene rilevato un rollback, e il timestamp non valido non verrà mai memorizzato nella cache come attendibile. Questo impedisce che un aggiornamento del timestamp rifiutato possa inquinare gli aggiornamenti futuri.
- <b>Causa principale:</b> Una <b>sequenza di aggiornamento impropria</b> (CWE-367: Time-of-check Time-of-use Race Condition, nel contesto della validazione dei metadati). Il controllo di Tough per il rollback dello snapshot nel timestamp veniva eseguito nel momento sbagliato, dopo che si erano verificati effetti collaterali (caching). Inoltre, la mancata validazione completa del contenuto del timestamp (lunghezza) rendeva la logica fragile.
- <b>Impatto:</b> Un timestamp dannoso (con una versione dello snapshot inferiore) potrebbe ingannare temporaneamente il client, facendogli memorizzare uno stato non valido. Ciò porta a un denial of service nel meccanismo di aggiornamento: il client in seguito tratterebbe gli aggiornamenti legittimi come non validi (falso rilevamento di rollback). Nessuna esecuzione diretta di codice o furto di dati, ma un <b>persistente mancato aggiornamento</b> può essere altrettanto pericoloso (ad esempio, impedire l'applicazione delle patch di sicurezza). (vedi [qui](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Rimedio:</b> Aggiornare a tough <b>0.20.0+</b> La versione corretta gestisce in modo sicuro gli eventi di rollback del timestamp. Se è stato osservato un errore di aggiornamento dovuto a questo bug, potrebbe essere necessario <b>cancellare i metadati memorizzati nella cache</b> (per rimuovere eventuali timestamp contaminati) prima di ritentare gli aggiornamenti con il client corretto. Come prassi generale, i client dovrebbero sempre verificare i metadati prima di considerarli attendibili o di memorizzarli nella cache: questo problema evidenzia tale principio.
Riferimenti:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Problema con tough, versioni precedenti alla 0.20.0 (CVE multipli)</i>
- [Voci del GitHub Advisory Database per CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [Voci del GitHub Advisory Database per CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [Voci del GitHub Advisory Database per CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [Voci del GitHub Advisory Database per CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Commit di patch di Tough v0.20.0:
- Commit di fix della versione root | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Commit di fix delle delegazioni di terminazione | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Commit di fix del rollback dello snapshot | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Commit di fix del rollback del timestamp | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)