Безопасность Solidity: все пути административного изменения
Перейдём от общей карты к конкретным записям. Для авторизации важно найти все пути изменения, а не только самый заметный publish. Новый helper, импорт, batch или исправление может обратиться к тому же storage. Если один путь пропускает проверку, защитный модификатор на другом не устанавливает общую безопасность ресурса.
Базовый CourseCatalog имеет constructor, publish и deactivate как пути предметной настройки. Constructor задаёт immutable publisher и отклоняет нулевой адрес. Оба внешних административных метода используют onlyPublisher. Чтение courses не меняет состояние и не нуждается в праве издателя.
Собственный ошибочный дополнительный метод
Рассмотрим специально неполный локальный фрагмент для чтения, не производственный код. Он показывает, как полезное название repair может скрыть новый путь записи. Остальные объявления берутся из базовой идеи; фрагмент не является самостоятельным файлом.
// Учебный контрпример: этот путь не имеет авторизации.
function repairHash(uint256 id, bytes32 digest) external {
courses[id].contentHash = digest;
}
Посторонний X может передать id 17 и другое значение, хотя основной publish защищён. Если запись была active, она останется active с заменённым hash. Установка bytes32(0) способна получить активную запись с нулевым отпечатком, которую исходный publish не создаёт.
Ошибка имеет два слоя: отсутствует нужный субъект и отсутствуют условия допустимого состояния. Добавление только onlyPublisher устранит свободный доступ, но оставит возможность уполномоченному редактору нарушить ненулевое условие. Для подходящего нового требования метод должен проверять оба слоя либо вообще не существовать.
Условие перед записью
Если продукт действительно хочет редактировать hash отдельно, сначала нужно определить семантику. Разрешено ли менять отозванный выпуск? Должен ли repair активировать курс? Какое событие описывает изменение? Нельзя просто скопировать присваивание и считать, что смысл совпал с publish.
В базовой модели подходящий путь уже существует: publish заменяет hash и делает active=true. Для сохранения inactive после правки понадобился бы явно другой метод и новое условие. Отсутствие такого требования является достаточным основанием сохранить маленький контракт без дополнительного setter.
Поэтому наш итог не «добавить максимально много проверок», а сопоставить каждому необходимому пути конкретный договор. Лишний административный маршрут увеличивает область влияния и усложняет историю. Если его польза не определена, автоматическое появление repair в интерфейсе лишь создаёт новую неопределённость.
Модификатор и тело
В onlyPublisher сравнивается msg.sender с выбранным адресом, затем выполняется тело через _. Отсутствие _ означало бы другое поведение: тело не исполняется. Проверка только названия modifier в декларации не достаточна; нужно прочитать его фактические строки и расположение перехода.
Если будущий modifier содержит код после _, его последствия тоже входят в вызов. Например, внешнее взаимодействие после публикации создаст дополнительную границу исполнения, которой нет в базовом реестре. Разбор авторизации и состояния должен учитывать весь унаследованный путь, а не только середину функции.
Внутренняя функция сама по себе не доступна по внешнему ABI. Но любой внешний wrapper, который её вызывает, определяет достижимость. Поэтому запись «helper internal» не доказывает безопасность, если свободный external метод передаёт ему произвольные аргументы без проверки.
Контракт как издатель
Publisher может быть контрактом. Тогда любой его разрешённый маршрут внешнего вызова к CourseCatalog заслуживает анализа. Если он предоставляет публичный execute(target,data) без ограничений и назначен publisher, посторонний посетитель может сформировать публикацию от его адреса.
CourseCatalog при этом честно видит разрешённого msg.sender. Ошибка находится в управляющем контракте, который передал пользователю своё полномочие. Исправление не сводится к замене проверки target на tx.origin: нужно ограничить маршрут исполнения выбранной предметной политикой.
Примером подходящей явной политики служит учебный комитет два из трёх из курса ролей. Он ограничивает цель, параметры и подтверждения. Но даже такой комитет имеет собственные пределы: нет срока и отмены. Правильный target caller не доказывает все требования жизненного цикла согласования.
Read и административная история
Публичное чтение записей не является утечкой административного разрешения. Состояние цепочки и так наблюдаемо. Запретить getter всем кроме publisher не означает скрыть contentHash от остальных; это только усложнит обычную интеграцию без установления настоящей секретности.
Событие изменения помогает обнаружить действие, но не предотвращает его. Если repair записал hash без события, мониторинг может пропустить путь; если событие добавлено, свободный вызов всё ещё остаётся свободным. Наблюдение и авторизация решают разные задачи, их нельзя заменять друг другом.
Также событие не является окончательным доказательством current state после последующих действий. Издатель мог опубликовать H1, затем H2 и отозвать курс. Анализ каждого перехода использует соответствующую точку состояния и ожидаемый договор, а не одну удобную строку из журнала.
Отрицательное и положительное условие
Для будущей проверки авторизации нужны оба направления. X не меняет запись. Правильный publisher может выполнить допустимую публикацию. Если отвергаются все вызовы, отсутствие чужих изменений не означает работоспособность редакционного пути. Отказ в обслуживании разрешённой функции тоже может быть нарушением требований.
Для проверки входов нужен аналогичный баланс. Нулевые id и digest отклоняются, а обычные 17 и H1 принимаются выбранным субъектом. Затем проверяется уже активный курс, повтор одинакового hash и повтор deactivate. Последние случаи определяются договором, а не общим ожиданием «всё повторное должно быть запрещено».
Разбор карты присваиваний
В самостоятельном задании выпишите все присваивания базового реестра. Для каждого укажите внешний достижимый метод, caller, входные условия и итог. Потом добавьте ошибочный repair только на бумаге и объясните, какое свойство перестаёт следовать из карты.
Третий вариант заменит publisher обычным публичным посредником. Проследите caller до target и найдите место, где должно ограничиваться намерение пользователя. Так авторизация становится анализом всех реальных маршрутов ресурса. В следующем уроке отдельно покажем, почему начальный отправитель транзакции не подходит для замены непосредственного субъекта.
Для самостоятельного уточнения: security-considerations.html, contracts.html.
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.