누락된 입력 검증 공격이란?
누락된 입력 검증 공격은 Solidity 계약이 사용자 제어 주소, 금액, 식별자, 가격 또는 설정값을 검증 없이 받아들일 때 발생하며, 계약의 경제적 및 보안 가정이 충족되는지 검증하지 않습니다. 이를 방지하려면, 자산 이동 또는 회계 변경 전에 허용 목록, 범위 체크, 영 주소 체크, 권한 확인, 상태 불변성을 통해 모든 외부 입력을 신뢰 경계에서 검증해야 합니다.
이 영향은 구체적입니다: 2022-01-28, BSC에서 Qubit이 누락된 입력 검증 사고로 인해 8천만 달러를 잃었습니다. 이러한 실패는 단순히 잘못된 거래 문제를 넘어서며, 검증되지 않은 값은 유효한 Solidity 코드가 잘못된 비즈니스 행동—존재하지 않는 담보를 크레딧하거나, 신뢰하지 않는 목적지로 자산을 이체하거나, 차주의 실제 한도를 초과하는 부채를 생성하는 것—을 실행하게 할 수 있습니다.
Solidity의 입력 검증 취약점이란 무엇이며, 공격은 어떻게 이루어지나?
Solidity에서 입력 검증 취약점은 계약이 호출자가 제공한 값을 신뢰할 때 발생합니다. 이 값이 권한, 자산 회계, 가격 또는 상태 전환을 제어하는 경우, 호출자가 ABI상으로 유효하지만 프로토콜상으로는 비합리적인 값을 공급하여, 그 상태를 활용해 자산 인출, 클레임 생성, 제한 우회 등을 수행하는 것입니다.
Solidity는 타입을 강제하지만, 프로토콜 의도는 강제하지 않습니다. 예를 들어, uint256은 어떤 비-리턴 숫자라도 될 수 있고, address는 EOA, 악성 계약, 제로 주소 또는 지원되지 않는 토큰을 가리킬 수 있습니다. 계약은 이러한 의미적 규칙을 수립해야 합니다.
일반적인 공격 시퀀스는 다음과 같습니다:
- 공격자가 검증되지 않은 금액, 토큰, 수신인, 가격 또는 식별자를 받는 공개 함수를 식별합니다.
- 그들은 프로토콜의 의도된 가정을 위반하는 값을 선택합니다.
- 계약이 안전하지 않은 잔액, 권리, 부채 한도 또는 승인 상태를 기록합니다.
- 이후 공격자는 이 독성 상태를 신뢰하는 함수 호출을 진행합니다.
- 프로토콜은 부정한 상태를 기반으로 자산을 이체하거나, 양도 가능한 클레임을 생성합니다.
- 다른 사용자들이 회계상 부족분이 드러날 때 손실을 입게 됩니다.
이 핵심 검토 질문은: 이 입력이 무엇을 나타내는지, 그리고 계약이 그 사실을 독립적으로 검증하는가? 입니다. 답변이 “사용자가 제공했다”라면, 이는 부적절한 입력 검증 취약점입니다.
어떤 Solidity 패턴이 입력 검증 공격을 유발하나요?
입력 검증 공격은 대부분 계약이 통합이나 사용자 경험을 위해 유연성을 허용하는 곳에서 발생하며, 이 유연성을 프로토콜이 지원하는 범위 내로 제한하지 않기 때문에 생깁니다. 위험한 파라미터는 종종 price 또는 amount처럼 명확히 명명되지 않으며, 토큰 주소, 경로, 수신인, 트랜치 ID, 증명 또는 외부 계약 주소일 수 있습니다.
일반적인 unsafe 패턴은 다음과 같습니다:
- 사용자 선언 담보 값: 차입자가 차입 가능 금액에 사용되는 값을 제공하는 경우.
- 임의의 토큰 주소: 승인되지 않은 ERC-20 토큰을 허용하는 예치함수.
- 검증되지 않은 수신인 주소:
address(0), 계약 자체 또는 규칙을 위반하는 주소를 허용하는 경우. - 무제한 퍼센티지 및 비율: 수수료, LTV, 할인, 슬리피지, 보상 비율이 의도된 기준점 범위를 초과하는 경우.
- 배열 관계 검증 미흡: 병렬 배열 길이 불일치, 중복 허용, 예상치 못한 자산 재사용 등이 포함됨.
- 전제조건 없는 상태 전환: 클레임, 취소, 상환, 이전이 잘못된 상태에서 수행됨.
- 외부 데이터 신뢰: 가격, 잔액, 서명 또는 반품 값을 검증 없이 신뢰하는 경우.
검증은 맥락에 따라 달라집니다. 예를 들어, require(amount > 0)은 유용하지만, 그 값이 감당 가능한 것인지, 담보인지, 상한 내인지, 또는 현재 상태와 일치하는지를 증명하지는 않습니다. 마찬가지로, 토큰 주소가 제로가 아닌 것만으로도 토큰이 승인되었거나 기대하는 동작을 보장하지 않습니다.
Soken의 검토 방법론은 모든 외부 파라미터를 상태 전이 시도라고 간주하고, 영향을 미칠 수 있는 불변 조건을 식별한 후, 그 함수가 그 불변 조건을 강제하는지 검증하는 방식을 따릅니다.
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), "zero asset");
asset = asset_;
}
function borrow(
uint256 collateralValue,
uint256 amount
) external {
require(collateralValue > 0, "no collateral");
require(amount <= collateralValue, "insufficient collateral");
// 취약점: 차입자가 담보 가치를 공급함.
debt[msg.sender] += amount;
require(asset.transfer(msg.sender, amount), "transfer failed");
}
}
개선된 설계는 실제로 받은 담보를 근거로 차입 가능 금액을 산출합니다. 불변 변수인 자산 주소는 제한된 허용 목록 역할을 하며, 생성자 검증은 제로 주소를 차단하고, LTV는 제한되어 있으며, 잔액 차이 검증은 요청된 예치금이 아닌 실제 수신된 담보를 기반으로 합니다.
// 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), "zero loan asset");
require(address(collateralAsset_) != address(0), "zero collateral asset");
require(maxLtvBps_ > 0 && maxLtvBps_ <= 10_000, "invalid LTV");
loanAsset = loanAsset_;
collateralAsset = collateralAsset_;
maxLtvBps = maxLtvBps_;
}
function depositCollateral(uint256 amount) external {
require(amount > 0, "zero amount");
uint256 beforeBalance = collateralAsset.balanceOf(address(this));
require(collateralAsset.transferFrom(msg.sender, address(this), amount), "transfer failed");
uint256 received = collateralAsset.balanceOf(address(this)) - beforeBalance;
// 수정 : 실제 수신된 담보만 고려
require(received == amount, "unsupported token behavior");
collateral[msg.sender] += received;
}
function borrow(uint256 amount) external {
require(amount > 0, "zero amount");
uint256 limit = (collateral[msg.sender] * maxLtvBps) / 10_000;
require(debt[msg.sender] + amount <= limit, "borrow limit exceeded");
debt[msg.sender] += amount;
require(loanAsset.transfer(msg.sender, amount), "transfer failed");
}
}
당신의 프로토콜이 유연한 자산, 루트, 또는 회계 입력을 허용한다면, 적대적 스마트 계약 감사 및 침투 테스트는 각 주장된 값이 자금에 영향을 미치기 전에 독립적으로 검증되었는지 추적할 수 있습니다.
어떻게 Solidity에서 입력 검증 공격을 방지할 수 있나요?
입력 검증 공격을 방지하려면, 입력값이 들어오는 시점에 검증을 수행하고, 그 후 계약의 전체 라이프사이클 동안 명시적 불변 조건을 유지해야 합니다. 가장 강력한 방어는 단일 수정자 하나가 아니라, 의미 있는 상태 변경마다 명확히 정의된 허용 입력 집합과 이후에 참인지 검증할 수 있는 조건이 존재하는 설계입니다.
| 기법 | 차단하는 내용 | 가스 또는 복잡성 비용 | 한계 |
|---|---|---|---|
| 토큰 및 구현 허용 목록 | 미승인 ERC-20, 악성 어댑터, 임의 외부 호출 | 저장소 조회 또는 불변 주소 비교 | 승인된 토큰도 경제적 위험을 초래할 수 있음 |
| 범위 체크 | 제로 금액, 과도한 비율, 유효하지 않은 기준점, 안전하지 않은 기한 | 저비용, 비교 몇 차례 | 범위만으로는 경제적 정합성 증명 불가 |
| 영 주소 및 수신인 체크 | 자산 소각, 불가한 소유권, 잘못된 보관 대상 | 저비용, 주소 비교 1회 | 제로 주소는 여전히 신뢰할 수 없음 |
| 권한 부여 및 라이프사이클 체크 | 잘못된 상태 또는 호출자 권한 문제 | 저장소 읽기, 역할 설계 | 역할은 안전한 관리 필요 |
| 잔액 차이 검증 | 잘못된 예치 크레딧, 지원되지 않는 토큰 행동 | 추가 토큰 잔액 읽기 | 리베이싱 자산 처리 필요 |
| 불변조건 검증 | 파산, 공급, 부채, 준비금 관계 붕괴 | 추가 읽기 및 산술 연산 | 완전한 경제 모델을 포괄하는 불변 필요 |
구성 가능한 프로토콜에서는, 생성 시와 사용하는 시점 모두에서 구성을 검증하는 것이 중요합니다. 거버넌스가 세팅한 파라미터는 생성 시 안전하더라도, 의존 관계 변경에 따라 불안전해질 수 있기 때문입니다. 업그레이드 가능 시스템에서는, 초기화자 파라미터를 공개 함수 입력만큼 엄격하게 검증해야 합니다.
보안 인사이트: Soken 방법론은
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는 2020년부터 2026년까지 총 52건의 스마트 계약 사고를 누락된 입력 검증으로 분류하며, 총 손실액은 3억 3천 5백만 달러에 달합니다. 이러한 폭넓은 사고 패턴을 감안할 때, Soken은 검증 검토를 단순 체크리스트가 아닌 경제적-보안적 연습으로 여깁니다.
잘못된 입력 검증 취약점을 어떻게 감지할 수 있나요?
모든 외부적으로 접근 가능한 파라미터를 추적하여, 그것이 영향을 미치는 저장소 기록, 외부 호출, 자산 이체로 이어지는지 검증하고, 유효하지 않지만 ABI상 유효한 값이 불변 조건을 위반하는지 테스트하는 방식으로 검출할 수 있습니다. 정적 분석은 일부 누락된 체크를 찾지만, 프로토콜 특유의 가정을 검증하는 시나리오 기반 테스트가 필수입니다.
이 검토 체크리스트를 활용하세요:
- 가치, 권한, 루트에 영향을 미치는
external및public함수 파라미터 목록화. - 각각의 주소가 제로가 아니고 승인되었으며 적절한 역할인지 식별.
- 금액이 프로토콜 한도, 잔액, 부채 한도, 공급 한도, 소수점 가정과 일치하는지 확인.
- 가격, 담보 가치, 잔액이 신뢰할 수 있거나 독립 검증된 출처에서 왔는지 검증.
- 배열의 길이, 유일성, 순서, 교차 배열 인덱스 관계 검증.
- 각 함수가 유효한 라이프사이클 상태 및 승인된 호출자만 허용하는지 검증.
- 수수료, 리베이싱, 비정상 ERC-20 행동에 대한 잔액 차이 계정 검증.
- 자산 이동 후 조건을 명시하고, 테스트 수행.
Slither는 위험한 외부 호출, 검증되지 않은 반환값, 접근 제어 약점을 식별하는 데 도움을 줍니다. 하지만 모든 프로토콜 불변조건을 추론하지는 않기 때문에, Foundry fuzzing을 활용하여 0값, 최대값, 중복 배열, 예상치 못한 주소, 지원하지 않는 토큰 등을 생성하고 검증하는 것이 좋습니다.
불변조건 검증은 특히 강력합니다. 예를 들어, 전체 미지급 부채가 설정된 차입 용량을 초과하지 않는지, 크레딧된 담보가 받은 자산을 초과하지 않는지, 또는 승인된 자산만 시장 회계에 영향을 미치는지 테스트합시다. 이러한 속성에 근거한 더 넓은 보안 검증 프로세스를 구축할 때 Soken Hub를 참고하세요.
입력 검증은 사용자 의도와 프로토콜 진실을 가르는 경계입니다. 계약은 호출자가 주장하는 내용만으로 크레딧, 승인, 가격 또는 이체를 수행해서는 안 됩니다. 검증된 상태에서 민감한 값을 도출하고, 허용 범위를 제한하며, 결과 상태를 경제적으로 안전하게 만드는 불변 조건을 반드시 지켜야 합니다.
마지막 실천 단계로, 코드베이스 내 모든 자산 이동 함수에 대해 파라미터-불변 맵을 생성하는 것을 추천합니다. 신뢰 출처, 허용 범위, 후조건이 문서화되지 않은 파라미터는 즉시 검토 대상입니다.