
Проблема с tough, версии до 0.20.0 (несколько CVE)
В марте 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.
}
Это означает, что делегат с более низким приоритетом (который следует игнорировать после завершающей делегации выше него) всё равно мог быть учтён и предоставить вредоносный целевой файл