
Issue with tough, versions prior to 0.20.0 (Multiple CVEs)
В марте 2025 года в библиотеке Tough от AWS Labs (Rust-клиент реализации TUF – The Update Framework) были раскрыты множественные уязвимости безопасности. Эти проблемы отслеживаются под идентификаторами CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 и CVE-2025-2888 и были исправлены в Tough версии 0.20.0. Инженеры по безопасности могут использовать этот документ для понимания технических деталей каждой уязвимости, первопричины в кодовой базе и патчей, которые их устраняют. Всем пользователям Tough < 0.20.0 настоятельно рекомендуется обновиться до v0.20.0 или более поздней версии.
Tough не проверял последовательность версий Root metadata в процессе обновления. Атакующий, контролирующий репозиторий (или обладающий возможностью man-in-the-middle), мог предоставить файл root-метаданных с неожиданным номером версии, что приводило к загрузке и доверию клиента к неверной версии. По сути, Tough не гарантировал, что версия новых root-метаданных ровно на единицу больше ранее доверенной версии, что нарушало требование TUF о непрерывной цепочке доверия. Это могло привести к атакам отката (rollback) или подмены (mix-and-match), когда старый (но правильно подписанный) root принимается как актуальный, что потенциально возвращает выведенные из доверия ключи подписи или истёкшее доверие.
tough/src/lib.rs. Подверженная уязвимости реализация проверяла только то, что версия нового root не меньше старой, вместо требования, чтобы она была следующей последовательной версией. Это упущение нарушает правило спецификации TUF, согласно которому клиенты должны загружать промежуточные версии root по порядку (без пропуска версий).Уязвимая реализация: В версии 0.19.x и более ранних, после загрузки нового root.json, Tough проверял подписи, но не обеспечивал строгую непрерывность версий. Он позволял версии нового root быть любым значением >= доверенной версии. Например, если текущим доверенным root была версия N, Tough принимал новый root с версией N+2 или выше (при условии, что подписи были действительны), пропуская N+1. Приведённый ниже фрагмент кода иллюстрирует проверку до исправления:```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
});
Атакующий мог бы воспользоваться этим, представив <b>корневые метаданные более высокой версии, которые на самом деле являются более старым набором ключей</b>. Tough принял бы их, думая, что это обновление, и [доверился бы содержимому, подписанному устаревшими ключами](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Исправленная реализация:</b> Исправленный код (в Tough 0.20.0) явно требует, чтобы версия нового корневого метаданного была ровно на единицу больше старой версии, см. [коммит 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). Он также гарантирует, что новая версия выше (предотвращает равенство или уменьшение) и не допускает скачка больше чем на +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
}
);
Это изменение гарантирует, что клиент загружает и применяет корневые метаданные по порядку (N, N+1, N+2, ...) без пропусков. Если встречается файл корневых метаданных с версией, не равной точно old_version+1, Tough теперь расценивает это как потенциальную атаку и прерывает обновление.
Tough некорректно обрабатывал «терминирующие» делегированные роли целей, как они определены в TUF. В репозитории TUF делегирование может быть помечено как терминирующее, что означает: если поиск цели доходит до этого делегирования и цель не найдена, поиск должен прекратиться (и не продолжаться с другими делегированиями с более низким приоритетом). (см. здесь) Из-за логической ошибки Tough не прекращал поиск в этом случае – он продолжал перебирать последующие делегирования, хотя должен был остановиться. Это могло позволить атакующему, контролирующему делегирование с более низким приоритетом, предоставлять содержимое для целей, которыми он не должен управлять, в обход установленных границ доверия.
tough/src/editor/targets.rs (и связанный код разрешения делегирований). Уязвимый код неправильно обрабатывал флаг terminating у делегирований. При переборе делегирований в поисках цели Tough переходил к следующему делегированию, даже если текущее было помечено как терминирующее и не содержало совпадений, что противоречит спецификации TUF.Уязвимая реализация: В Tough <0.20.0 логика find_target() просто рекурсивно обходила или перебирала все возможные делегированные роли, пока не находила цель или не исчерпывала все варианты. Она не устанавливала никакого флага и не прерывала выполнение при встрече с терминирующей ролью. Псевдокод старого поведения:```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.
}
Это означает, что делегат с более низким приоритетом (который следует игнорировать после завершающей делегации выше него) всё равно мог быть учтён и предоставить вредоносный целевой файл
<b>Исправленная реализация:</b> В исправленной версии добавлен механизм отслеживания завершения и корректной остановки поиска. Когда встречается завершающая делегация, не содержащая целевой файл, Tough теперь [немедленно выходит из цикла поиска](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). В обновлённом коде логический флаг (например, `terminated`) устанавливается при достижении завершающей роли и распространяется вверх по стеку вызовов. Например:```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;
}
Additionally, the recursive find_target calls now carry a terminated flag to ensure that once termination is signaled, никакие другие делегирования не рассматриваются в циклах более высокого уровня. Структуры RepositoryEditor::delegate_role и связанные с ними в Tough также были обновлены, чтобы хранить атрибут terminating и передавать его через логику поиска.
break при завершающем делегировании позволяло учитывать неавторизованные данные о целевых объектах.Логика Tough для обнаружения атак отката в метаданных snapshot была неполной. В частности, при обновлении роли Snapshot Tough должен проверять, что все ранее известные метаданные целевых объектов (включая делегированные целевые объекты) всё ещё присутствуют в новом snapshot и не откатываются к более старым версиям. (см. здесь) Tough обеспечивал это для корневого targets.json, но не делал этого для файлов метаданных делегированных целевых объектов. Эта брешь могла позволить атакующему удалить или откатить файл делегированного целевого объекта в метаданных snapshot репозитория без обнаружения, из-за чего клиент принимал устаревший (или отсутствующий) файл делегированного целевого объекта как актуальный.
tough/src/lib.rs (функция, которая загружает/применяет новые метаданные Snapshot). Уязвимый код проверял только непрерывность основной роли targets.json в snapshot, но не проходился по делегированным ролям, перечисленным в метаданных snapshot, для выполнения аналогичных проверок.Уязвимая реализация: В Tough <0.20.0 после получения нового snapshot.json клиент проверял, что роли root и snapshot не были откачены, и специально убеждался, что targets.json всё ещё присутствует. Он также проверял, что версия targets.json в новом snapshot была >= предыдущей версии. Однако если snapshot содержал делегированные целевые объекты (например, делегированные метаданные projects.json, user.json), клиент их не проверял. Например, изначально код делал что-то вроде:```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.)
Это означает, что если злоумышленник с доступом к репозиторию <b>удалил файл делегированных метаданных или откатил его к более старой версии</b>, клиент Tough не заметил бы этого – до тех пор, пока основной targets.json оставался нетронутым. В результате клиент мог загрузить устаревший целевой файл из этой делегации, не подозревая, что он должен был быть отклонён как откат.
<b>Исправленная реализация:</b> Версия 0.20.0 добавляет всесторонние проверки для <b>каждой роли, указанной в метаданных snapshot</b>. Новый код перебирает каждую запись в метаданных старого snapshot (включая все роли делегированных целей) и обеспечивает два условия для каждой: (1) роль по-прежнему существует в новом snapshot, и (2) её версия не уменьшилась. Если какая-либо роль отсутствует в новом snapshot или имеет более низкий номер версии, чем раньше, обновление отклоняется как потенциальная атака отката. Например:```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, … }
);
}
Перебирая все роли (где имя — это имя каждого файла метаданных, например targets.json, delegated-role.json и т. д.), клиент обнаружит, если какие-либо метаданные делегированных целей были удалены или откачены. Ошибки SnapshotRoleMissing и SnapshotRoleRollback сработают, если роль исчезла или её версия откатилась назад. Примечательно, что Tough теперь также явно проверяет, что снапшот содержит как минимум запись targets.json (в противном случае возникает ошибка SnapshotTargetsMetaMissing) в качестве проверки корректности.
Tough некорректно обрабатывал обнаруженный откат в роли Timestamp. Роль Timestamp в TUF периодически подписывает номер последней версии метаданных снапшота, чтобы помочь клиентам обнаружить, что они видят более старый снапшот (откат). Tough выполнял проверку отката для версии снапшота, содержащейся в timestamp, но только после кэширования новых метаданных timestamp локально. Если обнаруживался откат, Tough отклонял обновление, но уже сохранял недействительный timestamp как «последний» в своём кэше. Это означало, что вредоносный timestamp (с устаревшей версией снапшота) мог отравить кэш клиента. Последующие легитимные обновления могли затем выглядеть для Tough как откаты (поскольку в кэше хранился timestamp, указывающий на более высокую версию снапшота, чем новая), блокируя допустимые обновления.
tough/src/lib.rs (функция, которая загружает новый timestamp.json). Основная проблема заключалась в ошибке порядка операций: Tough обновлял локальный кэш/состояние новыми метаданными Timestamp до того, как полностью проверял, что версия снапшота внутри не старше той, которую он видел ранее.Уязвимая реализация: В Tough <0.20.0 при получении нового timestamp.json клиент разбирал его и записывал в кэш (помечая как текущий доверенный timestamp), а затем выполнял проверку отката по версии снапшота. Если версия снапшота в новом timestamp была ниже ранее записанной версии снапшота (что указывало на попытку отката), Tough регистрировал ошибку и отклонял этот цикл обновления — но кэш уже содержал «плохой» timestamp. В этом пути обработки ошибки удаление кэшированной записи не выполнялось. Таким образом, запись клиента о «доверенном» timestamp могла стать этим устаревшим значением.
Например, предположим, что последняя известная версия снапшота была 5. Злоумышленник мог предоставить метаданные Timestamp (с более высоким номером версии timestamp), подписывающие версию снапшота 4. Tough принял бы файл timestamp (закэшировав его), затем заметил бы, что снапшот 4 < 5, и завершил бы обновление с ошибкой — но теперь его кэш говорит: «последний timestamp указывает на снапшот 4». Когда следующим придёт корректный timestamp (снапшот 5 или 6), Tough увидит снапшот 5 относительно закэшированного снапшота 4 как ещё один откат (поскольку он ошибочно доверяет версии снапшота 4 в кэше как базовой) и, таким образом, отклонит даже допустимое обновление. В бюллетене AWS описывается этот цикл: «клиент кэширует метаданные timestamp, несмотря на то, что они были корректно отклонены при обнаружении отката… что впоследствии не позволяет tough получать допустимые обновления».
Исправленная реализация: Исправление гарантирует, что проверки отката выполняются до кэширования нового timestamp, и добавляет более строгую проверку содержимого timestamp. В Tough 0.20.0 функция load_timestamp() была изменена, чтобы до завершения обновления гарантировать, что метаданные Timestamp корректно сформированы и что версия его снапшота не меньше ранее доверенной версии снапшота. В частности, код теперь проверяет: (a) метаданные timestamp содержат ровно одну запись (она должна быть только для snapshot.json), (b) эта запись существует и разбирается, и (c) версия снапшота внутри >= более старой версии снапшота. (см. здесь) Только после прохождения этих проверок новый timestamp считается доверенным. Критически важная добавленная логика показана ниже:```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 } ); }
С этим изменением, если версия снапшота в новой метке времени ниже ранее виденной, `ensure!` завершится ошибкой <b>до</b> того, как новая метка времени будет сохранена как доверенная. Поднимается ошибка `OlderSnapshotInTimestamp`, чтобы прервать обновление. В результате Tough сохранит старую (корректную) метку времени в кэше при обнаружении отката, а повреждённая метка времени никогда не будет закэширована как доверенная. Это предотвращает сценарий, при котором отклонённое обновление метки времени загрязняет будущие обновления.
- <b>Корневая причина:</b> <b>Некорректная последовательность обновления</b> (CWE-367: гонка времени проверки и времени использования в контексте проверки метаданных). Проверка Tough на откат снапшота в метке времени выполнялась в неподходящий момент — после возникновения побочных эффектов (кэширования). Кроме того, неполная проверка содержимого метки времени (длины) делала логику хрупкой.
- <b>Влияние:</b> Вредоносная метка времени (с более низкой версией снапшота) могла временно обмануть клиент, заставив его сохранить некорректное состояние. Это приводит к отказу в обслуживании механизма обновления: клиент в дальнейшем будет считать подлинные обновления недействительными (ложное обнаружение отката). Прямого выполнения кода или кражи данных нет, но <b>постоянный сбой обновления</b> может быть не менее опасен (например, препятствовать применению исправлений безопасности). (см. [здесь](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Исправление:</b> Обновитесь до tough <b>0.20.0+</b>. Исправленная версия безопасно обрабатывает события отката метки времени. Если наблюдался сбой обновления из-за этой ошибки, возможно, потребуется <b>очистить кэшированные метаданные</b> (чтобы удалить отравленную метку времени) перед повторной попыткой обновления с исправленным клиентом. Как общая практика, клиенты должны всегда проверять метаданные перед тем, как доверять им или кэшировать их — эта проблема подчёркивает данный принцип.
Ссылки:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Проблема с tough, версии до 0.20.0 (несколько CVE)</i>
- [Записи базы данных GitHub Advisory для CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [Записи базы данных GitHub Advisory для CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [Записи базы данных GitHub Advisory для CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [Записи базы данных GitHub Advisory для CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Коммиты исправления Tough v0.20.0:
- Коммит с исправлением версии корня | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Коммит с исправлением завершающих делегирований | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Коммит с исправлением отката снапшота | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Коммит с исправлением отката метки времени | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)