Перейти к содержанию

Безопасность Solidity: модель угроз учебной библиотеки

Начнём анализ безопасности с уже знакомой учебной библиотеки. Базовый CourseCatalog принимает идентификатор курса и ненулевой отпечаток, публикует его от единственного immutable publisher и умеет деактивировать запись. Он не хранит пользовательские платежи, не вызывает внешний код и не меняет издателя. Поэтому первое действие — описать реальные активы и переходы этой модели.

Список известных уязвимостей полезен, но не заменяет понимание требований. Можно написать хороший разбор повторного входа и при этом пропустить неправильную авторизацию единственного setter. Можно объявить полную неизменность курса, хотя publish по договору специально заменяет его отпечаток. Без модели ожидаемого поведения нельзя отличить допустимое действие от нарушения.

Что библиотека защищает

Для нашего реестра важны три свойства. Только выбранный publisher меняет записи. Активный курс имеет ненулевой отпечаток и ненулевой идентификатор. Деактивация сохраняет отпечаток, чтобы прежний выпуск оставался различимым. Дополнительная цель приложения — показывать именно байты, соответствующие согласованному манифесту.

Последнее свойство шире контракта. CourseCatalog не получает файл и не вычисляет его SHA-256. Он принимает bytes32. Проверка файла находится в редакционном процессе и клиентском приложении. Поэтому безопасность общей библиотеки нельзя свести к одному модификатору Solidity, даже если он корректно ограничивает запись.

Подготовим собственную карту. Она относится к исходной модели, не к токенному контейнеру или финансовому сервису соседнего курса.

Объект Ожидаемый договор Возможное нарушение
Издатель Один выбранный адрес Посторонний путь записи
Текущий выпуск Ненулевые id и hash Некорректная активная запись
Отзыв active=false, hash сохранён Потеря следа выпуска
Манифест SHA-256 точных байтов Клиент показывает другие данные
Навигация Правильная сеть и экземпляр Чтение похожего чужого каталога

Каждая строка связывает актив с конкретным последствием. Общая фраза «сайт должен быть безопасным» не подскажет, какой переход проверить. Здесь можно предметно объяснить, кто способен вызвать действие и что именно пользователь увидит при ошибке.

Кто является доверенным субъектом

Publisher в исходном контракте доверен менять текущий hash и возвращать курс в active. Это намеренная власть. Если его ключ потерян или его управляющий контракт выполняет нежелательное действие, onlyPublisher всё ещё может работать правильно: фактический caller соответствует выбранному адресу, а редакционное намерение нарушено.

Такой случай отличается от ошибки кода, позволяющей произвольному X пройти проверку. Исправления тоже разные. Для утраченного доверия к immutable publisher может понадобиться новый экземпляр и миграция приложения. Для пропущенного модификатора нужен другой исходник и корректный доступный маршрут замены модели.

Издатель не становится доверенным источником любого текста интерфейса автоматически. Если браузер получил данные из стороннего RPC либо произвольного URL, нужно отдельно проверить сеть, адрес, состояние и байты. Публичный контракт и его страницы имеют разные границы обработки внешней информации.

Входы и достижимые состояния

Для publish внешними данными являются courseId, contentHash и контекст вызова. Для deactivate — courseId и caller. Конструктор получает publisher_. Getter читает состояние по ключу, включая никогда не записанный ключ. Каждый вход рассматривается отдельно от формы кнопки на сайте.

Незнакомый числовой id с ненулевым hash может быть опубликован выбранным издателем. Реестр не проверяет, существует ли соответствующая Markdown-страница в ProfessorWeb. Если требуется связь с каталогом сайта, она находится в другом договоре. Нельзя назвать отсутствие такой проверки неожиданной дырой, пока требование не было установлено.

Повтор publish с тем же hash тоже допустим. Контракт не хранит правило уникальности выпусков и не отклоняет повторение. Так событие может появиться ещё раз при прежнем текущем значении. Индексатору нужно понимать, что количество Published не обязательно равно количеству различных файлов.

Последствия и приоритет

Посторонняя публикация способна подменить текущий реестровый ориентир. Недоступный файл мешает чтению, но не обязательно меняет on-chain state. Неправильное отображение inactive может показать отозванный материал как действующий. Разные последствия требуют разной срочности и разных мер.

Приоритет зависит от реального использования. У публичной учебной демонстрации последствия иной силы, чем у системы, выдающей дорогостоящую услугу по тому же флагу. В этом курсе мы не переносим лабораторные контракты в производственное хранение средств и не обещаем независимый аудит всех их расширений.

Полезно указать и невозможный в текущей модели сценарий. Базовый CourseCatalog не передаёт ETH и не делает внешние calls, поэтому обычная цепочка повторного входа через получателя платежа к нему не относится. Позже мы введём отдельный RefundLedger, чтобы изучить эту тему на соответствующем коде.

Профиль и подтверждение

Закрепляем Solidity 0.8.30, EVM Cancun и Foundry 1.3.1 как учебный профиль. Для дополнительного ReentrancyGuard используем OpenZeppelin 5.4.0. Эти версии описывают примеры, а не утверждение, что инструменты являются последними или что их код автоматически лишён риска.

В текущей работе листинги и выводы рассматриваются редакционно. Компиляция, проверки и сети не запускаются по условию пользователя. Поэтому ожидаемое поведение записывается как ожидаемое; отсутствие выполненного сценария нельзя заменить словами «всё прошло» или «уязвимость подтверждена».

Для будущей проверки модель угроз превращается в конкретные сценарии: X пытается publish; publisher публикует нулевой hash; publisher отзывает активный курс; клиент получает файл с другими байтами. Каждый сценарий должен иметь наблюдаемый результат и указание компонента, который обеспечивает условие.

Самостоятельный разбор

Возьмите курс 17 с (H1,true). Опишите допустимый переход к (H2,true), затем к (H2,false). Укажите субъекта, аргументы и состояние после каждого. Теперь замените publisher на постороннего X в намерении вызова и объясните ожидаемое отклонение без изменения записи.

После этого оставьте caller правильным, но подмените веб-файл по прежнему URL. Какой компонент обнаружит несоответствие SHA-256? Контракт не узнаёт о загрузке браузером, поэтому требование к целостности нельзя приписать ему одному. Эта карта станет основой следующего урока о границах доверия.

Для самостоятельного уточнения: security-considerations.html.

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

Оглавление серии