Як запобігти атакам на валідацію введення в Solidity

Автор статті

Відсутні атаки через неправильну валідацію вхідних даних

Атаки, пов’язані з відсутністю перевірки вхідних даних, виникають, коли Solidity-контракт приймає адреси, суми, ідентифікатори, ціну або конфігураційні значення, контрольовані користувачем, не підтверджуючи їх відповідність економічним та безпековим припущенням контракту. Щоб їх уникнути, слід перевіряти кожен зовнішній вхідний параметр на межі довіри за допомогою allowlistів, перевірок меж, перевірок нульових адрес, авторизацій та invariants стану перед перетином активів або внесенням змін до обліку.

Значення є конкретним: 2022-01-28 Qubit втратила $80.0M на BSC у інциденті, класифікованому як Missing Input Validation. Такі збої — це не просто проблеми з некоректними транзакціями. Неверифіковане значення може змусити валідний Solidity-код виконати некоректну операцію, наприклад, зарахувати неіснуючий заставний актив, переказати активи на ненадійний адрес або створити борг понад реальний ліміт позичальника.

Що таке вразливість у валідації вхідних даних у Solidity і як працює атака?

Вразливість у валідації вхідних даних у Solidity виникає, коли контракт ставиться до значення, яке надає викликатель, як до надійного, хоча це значення контролює дозволи, облік активів, ціну або стан переходів. Зловмисник надає значення, яке є синтаксично дійсним для ABI, але економічно некоректним для протоколу, і використовує отриманий стан для зняття активів, створення претензій або обходу обмежень.

Solidity застосовує типи, але не визначає наміри протоколу. uint256 може бути будь-яким не відкотним числом, а address — це може бути EOA, злоумисний контракт, нульовий адрес або не підтримуваний токен. Контракт має забезпечити дотримання відповідних семантичних правил.

Типовий сценарій атаки:

  1. Зловмисник ідентифікує публічну функцію, яка приймає неперевірену суму, токен, отримувача, ціну або ідентифікатор.
  2. Вибирає значення, яке порушує умови протоколу.
  3. Контракт записує небезпечний баланс, право або стан боргу.
  4. Зловмисник викликає пізнішу функцію, яка довіряє отруєному стану.
  5. Протокол переводить активи або створює претензію, яка може бути передана, на основі неправдивого стану.
  6. Інші користувачі несуть збитки, коли облік несподівано виявляється неправильним.

Ключове питання: який факт намагається представити це вхідне значення, і де контракт самостійно підтверджує цей факт? Якщо відповідь — «він його отримав від викликача», — у контракту є неправильна вразливість у валідації вхідних даних.

Які шаблони у Solidity сприяють атакам через погану валідацію?

Атаки на валідацію зазвичай виникають там, де контракт дозволяє гнучкість для інтеграцій або користувацького досвіду, але не обмежує цю гнучкість у підтримуваному домені протоколу. Небезпечний параметр не обов’язково називається price або amount; він може бути адресою токена, маршрутом, отримувачем, ідентифікатором трейна, доказом або зовнішнім адресом контракту.

Розповсюджені незахищені шаблони включають:

  • Заставна вартість, оголошена викликом: Позичальник надає значення, що використовується для обчислення його кредитної спроможності.
  • Довільні адреси токенів: Функція внесення депозиту приймає будь-який ERC-20, включаючи токени з нестандартною поведінкою при трансфері.
  • Неконтрольовані адреси отримувача: Функція приймає address(0) або адресу самого контракту або адресу, яка порушує правила збереження.
  • Необмежені проценти та коефіцієнти: Збір, LTV, дисконт, slippage або ставка винагороди виходять за межі своїх базових значень.
  • Некоректні структури масивів: Паралельні масиви мають різну довжину або містять дублікати, або маршрут повторює актив несподівано.
  • Перехід стану без передумов: Акти скасу, викупу, міграції працюють у некоректних станах життєвого циклу.
  • Зовнішні дані як факт: Функція довіряє цінами, балансам, підписам або значенням, поверненим без перевірки.

Перевірка повинна бути контекстуальною. require(amount > 0) корисне, але не доводить, що amount є доступним, забезпеченим заставою, у межах ліміту або узгоджуються з поточним станом. Аналогічно, перевірка, що адресу токена не нульова, не підтверджує, що токен затверджений, працює передбачувано або належить правильному ринку.

Методологія Soken підходить до кожного зовнішнього параметра як до спроби змінити стан: визначте invariants, які він може впливати, і переконайтеся, що функція їх забезпечує перед записом у пам’ять.

Що виглядає як приклад атаки через погану валідацію у Solidity?

Приклад такої атаки дозволяє позичальнику представити власне collateralValue. Контракт перевіряє, що запитана позика не перевищує цю суму, але він ніколи не підтверджує, що позичальник фактично депонув заставу на цю суму. Таким чином, викликач може встановити обидва значення на довільний рівень і позичити активи, профінансовані кредитором.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
}

contract UnsafeLender {
    IERC20 public immutable asset;
    mapping(address => uint256) public debt;

    constructor(IERC20 asset_) {
        require(address(asset_) != address(0), "нульовий актив");
        asset = asset_;
    }

    function borrow(
        uint256 collateralValue,
        uint256 amount
    ) external {
        require(collateralValue > 0, "немає застави");
        require(amount <= collateralValue, "недостатня застава");

        // УРАЗЛИВИЙ_МЕТОД: позичальник надає значення, що підтримує позику.
        debt[msg.sender] += amount;

        require(asset.transfer(msg.sender, amount), "невдалий переказ");
    }
}

Виправлений дизайн отримує можливість позики з застави, яку контракт фактично отримав. Адреси в asset є вузьким allowlistом, перевірки конструктора відхиляють нульові адреси, LTV обмежений, а перевірка зміщення балансу підтверджує invariant переказу, а не довіру до запитаної суми внеску.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

contract ValidatedLender {
    IERC20 public immutable loanAsset;
    IERC20 public immutable collateralAsset;
    uint256 public immutable maxLtvBps;

    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;

    constructor(IERC20 loanAsset_, IERC20 collateralAsset_, uint256 maxLtvBps_) {
        require(address(loanAsset_) != address(0), "нульовий актив позики");
        require(address(collateralAsset_) != address(0), "нульовий заставний актив");
        require(maxLtvBps_ > 0 && maxLtvBps_ <= 10_000, "недійсний LTV");
        loanAsset = loanAsset_;
        collateralAsset = collateralAsset_;
        maxLtvBps = maxLtvBps_;
    }

    function depositCollateral(uint256 amount) external {
        require(amount > 0, "нульова сума");
        uint256 beforeBalance = collateralAsset.balanceOf(address(this));
        require(collateralAsset.transferFrom(msg.sender, address(this), amount), "невдалий переказ");
        uint256 received = collateralAsset.balanceOf(address(this)) - beforeBalance;

        // ВИРІШЕННЯ: рахувати тільки фактично отриману заставу.
        require(received == amount, "несподівана поведінка токена");
        collateral[msg.sender] += received;
    }

    function borrow(uint256 amount) external {
        require(amount > 0, "нульова сума");
        uint256 limit = (collateral[msg.sender] * maxLtvBps) / 10_000;
        require(debt[msg.sender] + amount <= limit, "перевищено ліміт боргу");

        debt[msg.sender] += amount;
        require(loanAsset.transfer(msg.sender, amount), "невдалий переказ");
    }
}

Якщо ваш протокол підтримує гнучкі активи, маршрути чи облік, аудит і тестування зловмисних смарт-контрактів допоможе простежити, чи кожне заявлене значення незалежно підтверджується перед впливом на кошти.

Як запобігти атакам через неправильну валідацію вхідних даних у Solidity?

Щоб уникнути таких атак, потрібно перевіряти вхідні дані при вході і зберігати ці припущення через явні invariants протягом всього життєвого циклу контракту. Найкращий захист — не один модифікатор, а проектування, у якому кожна суттєва зміна стану має чітко визначений набір дозволених вхідних значень і перевірений стан, що залишається істинним після.

Техніка Що вона зупиняє Витрати газу або ускладнення Обмеження
Allowlist(и) токенів і реалізацій Не підтримувані ERC-20, зловмисні адаптери, сторонні виклики Перевірка з пам’яті або порівняння з immutable-адресою дозволені токени все ще можуть нести економічний ризик
Перевірки меж Нульові суми, надмірні ставки, некоректні базові проценти, небезпечні дедлайни Низькі, кілька порівнянь Одні межі не доводять економічної коректності
Перевірки нульових адрес і отримувачів Згоряння активів, неможливе управління правами, некоректні адреси отримувачів Низькі, одне порівняння адрес Нульова Address може бути довіреною, але не гарантованою
Авторизація та перевірки життєвого циклу Виклики у неправильному стані або не від авторизованих осіб Читання пам’яті, рольова модель Ролі вимагають безпечного адміністрування
Перевіркаdelta балансу Фальшиві кредити за депозитами і неладне поведінка при трансфері Додаткові перевірки балансу активу Потребує акуратної обробки активів з rebasing
Перевірка invariants Несправна платоспроможність, співвідношення пропозиції, боргу і резервів Додаткові читання і арифметика invariants повинні охоплювати весь економічний контекст

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

Захисний порада: методологія Soken починається з invariants, а не з require. Якщо рецензенти не можуть сформулювати, що має залишатися істинним після обробки вхідних даних, що контролюються користувачем, вони не зможуть визначити достатність перевірки.

Які реальні інциденти класифікувалися як відсутність валідації вхідних даних?

Відсутність валідації вхідних даних асоціювалася з суттєвими втратами смарт-контрактів у сферах кредитування, свопінгу та управління активами. Категорія інцидентів охоплює різні ланцюги та дизайни протоколів, що свідчить: неправильна валідація — це проблема логіки протоколу, а не дефект конкретної мережі або Solidity.

Протокол Дата Втрачені суми Ланцюг
Qubit 2022-01-28 $80.0M BSC
Hedgey 2024-04-19 $44.7M Arbitrum, Ethereum
Transit Swap 2022-10-02 $28.9M Ethereum, BSC
Pickle 2020-11-22 $19.7M Ethereum

Ці інциденти ще раз підтверджують виведення: контракти не можуть безпечно робити висновки з істини протоколу лише тому, що транзакція успішно закодована і передає цінність. Системи активів потребують семантичної верифікації щодо конкретної властивості, що захищає цінність — будь-то отриманий заставний актив, підтримуваний актив, авторизований маршрут або правильний стан життєвого циклу.

DefiLlama підсумовує 52 інциденти смарт-контрактів за 2020–2026 роки під категорією Missing Input Validation із сумарними втратами $330.5M. Саме тому Soken розглядає перегляд валідності як економічну та безпекову практику, а не як список поверхневих require.

Як виявити неправильні вразливості у валідації вхідних даних?

Можна виявити неправильну валідацію, прослідкувавши кожен зовнішній параметр до записів у пам’ять, зовнішніх викликів і трансферів активів, які він впливає, і протестувавши, чи будь-яке недійсне, але валідне для ABI, значення може порушити invariants. Статичний аналіз допомагає знайти деякі відсутні перевірки, але сценаріївські тести необхідні для перевірки протокол-специфічних припущень.

Використовуй цей чекліст:

  1. Вкажіть кожен external та public параметр функції, який впливає на цінність, повноваження або маршрутизацію.
  2. Оцініть, чи кожна адреса є ненульовою, затвердженою і підходить для своєї ролі.
  3. Перевірте кожну суму за межами протоколу, балансами, обмеженнями боргу, лімітами пропозиції і припущеннями щодо десяткових.
  4. Переконайтеся, що ціни, заставні значення і баланси отримані з надійних або незалежно підтверджених джерел.
  5. Перевіряйте довжину масивів, унікальність, порядок і зв’язки між індексами масивів.
  6. Забезпечте, щоб кожна функція дозволяла лише допустимі стані життєвого циклу та авторизованих викликаючих.
  7. Тестуйте облік delta балансу з комісіями, rebasing та незвичайною поведінкою ERC-20.
  8. Описуйте і тестуйте пост-умову для кожної операції переміщення активів.

Slither допомагає ідентифікувати шаблони, такі як небезпечні зовнішні виклики, невірні повернені значення та слабкий контроль доступу, що часто оточують проблеми валідації. Вони не можуть вивести кожен invariants, тому рекомендується використовувати Foundry fuzzing для генерації нульових значень, максимальних, дубльованих масивів, несподіваних адрес чи не підтримуваних токенів.

Особливо ефективні invariants-тести: наприклад, переконайтеся, що загальний борг ніколи не перевищує налаштовану позикову спроможність, що зарахований заставний капітал ніколи не перевищує отриманих активів, і що лише затверджені активи можуть змінювати облік ринку. Звертайтесь до Soken Hub для ширшого формування процесу безпеки, що базується на цих властивостях.

Валідація вхідних даних — це кордон між наміром користувача й правдою протоколу. Контракт ніколи не повинен зараховувати, авторизовувати, оцінювати ціну або переказувати гроші лише на основі заяви викликача. Виводьте чутливі значення з перевіреного стану, обмежуйте домени прийняття і забезпечуйте invariants, що забезпечують економічну безпеку стану.

Практичний наступний крок — створити карту параметрів до invariants для кожної функції переміщення активів у вашій кодовій базі. Любий параметр без документованого джерела довіри, дозволеного діапазону і пост-умови вимагає негайної перевірки.

Автор статті

Часті запитання

Що таке вразливість у валідації введення в Solidity?

Вразливість у валідації введення в Solidity виникає, коли контракт приймає у контроль користувача адреси, суми, ідентифікатори, ціни чи конфігураційні значення без підтвердження, що вони відповідають його економічним і безпековим припущенням. Це може призвести до виконання некоректних дій.

Як уникнути атак через неправильну валідацію введення в Solidity?

Щоб запобігти цим атакам, потрібно перевіряти кожен зовнішній вхід на межі довіри перед переказом активів або обліком. Використовуйте allowlists, bounds checks, zero-address checks, авторизаційні перевірки та invariants, щоби кожне значення відповідало безпековим і економічним правилам контракту.

Що сталося з атакою через неправильну валідацію введення у Qubit?

Qubit втратила $80M на BSC 28 січня 2022 року через інцидент із відсутністю валідації вводу. Це показує, що необроблені входи можуть спричинити некоректне визнання collateral або виконання неправомірних економічних дій.

Чому контракти Solidity мають перевіряти zero address?

Контракти Solidity мають відкидати zero address, бо він не повинен бути використаний як дійсне місце призначення, актив або влада. Це запобігає прийому placeholder значень, що порушують передачу, авторизацію або облік.

Чи достатньо bounds checks для захисту від вразливостей у валідації?

Одних bounds checks недостатньо, оскільки значення може бути у межах, але все ще порушувати авторизацію, allowlist, адресу або стан. Потрібна багаторівнева перевірка на межі довіри перед переказом активів або змін обліку.

Чат