손상된 접근 제어 공격은 Solidity 계약이 자산 이동, 구현 변경, 역할 할당 또는 신뢰할 수 있는 주소 구성과 같이 제한되어야 하는 상태 변경 능력에 대해 신뢰할 수 없는 호출자가 접근하도록 허용할 때 발생합니다. 공격자는 암호학을 깨뜨릴 필요가 없으며, 오히려 누락되거나 잘못된 또는 지나치게 포괄적인 권한 검사 기능을 이용하여 실질적인 권한 상승을 이끕니다.
이 영향은 프로토콜 전체에 영향을 미칠 수 있는데, 권한이 부여된 함수들이 종종 풀링된 자금이나 거버넌스에 중요한 구성요소를 제어하기 때문입니다. Poly Network는 2021년 8월 10일에 Ethereum, BSC, Polygon에서 6억 1천 1백만 달러를 잃었으며, 이는 DefiLlama가 부적절한 접근 제어로 분류한 사건입니다. 따라서 권한 부여는 UI 문제 또는 행정적 세부 사항이 아니며, 계약의 핵심 보안 범위의 일부입니다.
Solidity에서 손상된 접근 제어 취약점이란 무엇인가?
손상된 접근 제어 취약점은 계약이 msg.sender의 권한을 신뢰성 있게 검증하지 않거나, 권한 부여 모델이 해당 작업에 필요 이상으로 많은 권한을 부여할 때 존재합니다. Solidity에서는 이 실패가 주로 민감한 저장소를 변경하거나, 자산을 전송하거나, 코드를 업그레이드하거나, 신뢰할 수 있는 상대방을 변경하는 외부 호출 가능 함수에서 나타납니다.
계약이 owner 변수만 있더라도 취약할 수 있습니다. 중요한 점은 모든 권한이 부여된 실행 경로가 helper 함수, 초기화 함수, 역할 관리 함수, 업그레이드 훅 등을 포함하여 해당 권한을 올바르게 검증하는지 여부입니다.
일반적인 손상된 접근 제어 공격은 다음 순서를 따릅니다:
- 공격자는 권한이 부여된 영향을 미치는 외부 호출 가능 함수를 식별합니다.
- 이 함수는 호출자 검증이 없거나 잘못된 주소를 검증하거나, 공격자가 제어하는 상태에 의존합니다.
- 공격자는 프론트엔드 제한을 우회하여 직접 함수를 호출합니다.
- 이 호출은 역할을 부여하거나 신뢰할 수 있는 주소를 변경하거나 자산 이동 작업을 호출합니다.
- 공격자는 새롭게 확보된 능력을 이용해 자금을 빼내거나 프로토콜 동작을 변경합니다.
- 계약은 이더리움 가상 머신(EVM)이 온체인 검증만 강제하기 때문에 트랜잭션을 수락합니다.
프론트엔드 버튼, 문서, 또는 의도된 운영 절차는 직접 계약 호출을 제한하지 않습니다. 어떤 계정이라도 공개 또는 외부 함수를 대상으로 calldata를 제출할 수 있기 때문입니다. 따라서 Solidity 코드는 민감한 상태 전환이 발생하는 정확한 지점에서 정책을 강제로 적용해야 합니다.
어떤 Solidity 패턴이 접근 제어 공격 및 권한 상승을 유발하는가?
부적절한 접근 제어 취약점은 개발자가 구성 세터, 행정 도우미 또는 초기화 경로를 공개할 때 자주 발생하며, 여기서 모든 공개 함수가 공격자에 노출된 API임을 고려하지 않는 경우입니다. 이 위험한 패턴은 onlyOwner 수정자 자체가 없거나, 임의 호출자가 변경 가능한 권한, 잘못 상속된 권한, 일관성 없는 적용, 또는 작업에 비해 너무 광범위한 권한 부여를 포함합니다.
일반적인 실패 패턴은 다음과 같습니다:
- 누구나 재무, 오라클, 구현, 운영자 또는 수혜자를 교체할 수 있는 공개 세터.
- 관리자 검증이 없는 역할 관리 함수.
- 배포 후 호출 가능하거나 예기치 않은 계정에서 호출 가능한 초기화 함수.
tx.origin대신msg.sender를 검증하는 함수.- 보호되지 않은 외부 래퍼를 통해 접근하는 권한이 부여된 내부 함수.
- 모든 운영 작업을 수행할 수 있는 단일 관리자 역할, 공격 범위 확대.
- 불변하거나 통제된 역할 매핑 대신 사용자 제공 주소를 기반으로 하는 권한 부여.
- 호출자를 검증하지만 새 구현 또는 업그레이드 경로는 검증하지 않는 업그레이드 함수.
이 결함 패턴은 특히 미묘합니다:
function setOperator(address newOperator) external {
operator = newOperator;
}
지불 함수는 msg.sender == operator를 올바르게 요구할 수 있지만, 접근 제어의 경계는 이미 실패했으며 누구든지 자신을 operator로 지정할 수 있습니다. 검토자는 민감한 함수마다 역으로 추적하여 누가 이 역할을 획득할 수 있는지 파악해야 합니다.
Soken 방법론은 역할 할당과 신뢰할 수 있는 주소 구성을 별개의 권한 부여 작업으로 간주합니다. 안전한 접근 제어 설계는 최종 자산 이체뿐 아니라 이를 잠금 해제할 수 있는 모든 경로를 보호합니다.
Solidity 코드에서 손상된 접근 제어 공격은 어떻게 보이나?
이 취약한 금고는 지급 전에 호출자를 검증하지만, setPayoutOperator 함수는 공개되어 있습니다. 공격자는 자신을 운영자로 지정한 후 pay를 호출하여 이후의 권한 검증을 무의미하게 만듭니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerablePayoutVault {
address public payoutOperator;
constructor(address initialOperator) {
payoutOperator = initialOperator;
}
receive() external payable {}
function setPayoutOperator(address newOperator) external {
// 취약점: 누구든지 지급 권한을 부여할 수 있음.
payoutOperator = newOperator;
}
function pay(address payable recipient, uint256 amount) external {
require(msg.sender == payoutOperator, "not operator");
require(address(this).balance >= amount, "insufficient balance");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "payment failed");
}
}
이 문제를 해결하려면, 관리와 업무를 분리해야 합니다. 오직 소유자만이 좁은 범위의 지급 역할을 부여하거나 재약화할 수 있어야 하며, 지급 운영자는 역할을 관리하거나 소유권을 변경할 수 없습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FixedPayoutVault {
address public owner;
mapping(address => bool) public payoutOperators;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier onlyPayoutOperator() {
require(payoutOperators[msg.sender], "not payout operator");
_;
}
constructor(address initialOwner, address initialOperator) {
require(initialOwner != address(0), "zero owner");
require(initialOperator != address(0), "zero operator");
owner = initialOwner;
payoutOperators[initialOperator] = true;
}
receive() external payable {}
function setPayoutOperator(address operator, bool allowed) external onlyOwner {
require(operator != address(0), "zero operator");
// 수정: 오직 소유자만 권한 변경 가능.
payoutOperators[operator] = allowed;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero owner");
owner = newOwner;
}
function pay(address payable recipient, uint256 amount)
external
onlyPayoutOperator
{
require(address(this).balance >= amount, "insufficient balance");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "payment failed");
}
}
역할 변경, 업그레이드 권한, 또는 재무 출금이 시스템에 존재한다면, 접근 제어 중심 스마트 컨트랙트 감리 및 보안 평가가 이러한 경로들이 적절히 제약되어 있는지도 추적할 수 있습니다.
Solidity 개발자가 부적절한 접근 제어 취약점을 어떻게 방지할 수 있나?
손상된 접근 제어 공격을 방지하려면, 완성 후에 흩어져 있던 require문을 추가하는 것보다 명확한 권한 부여 모델이 필요합니다. 어떤 행위자가 어떤 민감한 작업을 수행할 수 있는지, 그 권한이 존재하는 이유, 그리고 어떤 작은 역할이 일상 업무를 안전하게 수행할 수 있는지 정의해야 합니다.
| 기법 | 어떤 것을 멈추나 | 가스 또는 복잡성 비용 | 한계점 |
|---|---|---|---|
onlyOwner 수정자 |
소유자 전용 함수의 무단 행정 | 보호 호출당 하나의 저장소 읽기 | 손상된 소유자는 여전히 전체 권한 보유 |
역할 매핑 또는 OpenZeppelin AccessControl |
별도 운영 역할의 무단 사용 | 저장소 읽기와 역할 관리 로직 | 잘못된 역할 설계는 과도한 권한 부여 가능성 존재 |
| 최소 권한 역할 | 관련 없는 관리 권한을 사용하는 관리자 또는 운영자 | 더 많은 함수와 역할 정의 필요 | 명확한 운영 경계 필요 |
| 중요 변경에 대한 타임록 | 악의적 업그레이드 또는 파라미터 변경 지연 | 지연된 실행과 추가 계약 | 사용자가 대응할 수 없으면 지연 후 악의적 행위 방지 X |
| 다중 서명 제어 행정 | 단일 키가 일방적으로 권한 상태 변경 | 서명자 조정 및 운영 오버헤드 | 공유 서명자 또는 취약 정책은 계속 위험 |
| 일회성 초기화 가드 | 무단 또는 반복 초기화 | 저장소 플래그와 초기화 로직 | 배포 절차는 여전히 의도된 권한 미설정 필요 |
적절한 검증이 완료된 소유권과 역할 프리미티브를 사용하되, 라이브러리를 전체 보안 모델로 간주하지 마십시오. 예를 들어 OpenZeppelin Ownable은 소유권 구조를 제공하지만, 프로토콜팀은 여전히 소유권이 일시정지, 업그레이드, 수수료 수집, 오라클 교체 또는 긴급 복구를 제어하도록 결정해야 합니다.
tx.origin을 인증 용도로 사용하는 것은 피하십시오. 이는 중개 계약을 통해 영향을 받을 수 있으며, 호출자를 직접 식별하지 못하기 때문입니다. 대신 msg.sender를 사용하고, 이를 미리 정의된 소유자, 역할, 다중 서명 또는 계약별 권한 규칙에 대해 검증하세요.
실제 사고 사례는 어떤 것이 있으며, 접근 제어 공격의 영향을 어떻게 보여주나?
DefiLlama는 2020년부터 2026년까지 교환 및 지갑 유출을 제외하고, 부적절한 접근 제어로 분류된 130건의 스마트 컨트랙트 사고를 기록하며, 총 피해액은 9억 97만 달러입니다. 아래는 검증된 사고 사례로, 이들은 브리지, DeFi 프로토콜, 봇, 애플리케이션 등 여러 체인에 걸친 접근 제어 실패를 보여줍니다.
| 프로토콜 | 날짜 | 금액 | 체인 |
|---|---|---|---|
| Poly Network | 2021-08-10 | 6억 1천 1백만 달러 | Ethereum, BSC, Polygon |
| Mirror | 2021-10-08 | 9천만 달러 | Terra |
| Wintermute | 2022-06-05 | 1천 760만 달러 | Optimism |
| ValueDefi | 2021-05-05 | 1천만 달러 | BSC |
| Cork V1 | 2025-05-28 | 1천200만 달러 | Ethereum |
| SUPERFORTUNE AI | 2026-05-27 | 1천520만 달러 | Ethereum |
| The Sandbox | 2026-08-22 | 675천 달러 | Base, BSC, Ethereum |
| Radix | 2026-08-31 | 120만 달러 | Radix |
이 사고들은 공통된 보안 교훈을 전달합니다. 즉, 계약이 의도한 신뢰 모델과 일치하지 않는 권한 경계로 인해 프로토콜의 경제적 가치가 노출될 수 있다는 점입니다. 피해를 입은 구체적인 함수는 다를 수 있지만, 실패 유형은 같습니다: 설계가 부여하지 말았어야 할 권한이 누군가에게 부여되거나 행사되는 경우입니다.
배포 전 손상된 접근 제어 취약점을 어떻게 탐지할 수 있나?
접근 제어 취약점 탐지는 민감한 상태 변화와 그 승인 조건 사이의 맵핑을 하고, 그 조건을 우회할 수 있는 대체 호출 경로가 존재하지 않음을 증명하는 과정을 포함합니다. 자동화 도구는 의심스러운 패턴을 식별하는 데 도움을 주지만, 실질적 권한 부여, 신뢰 가정, 권한 상승 체인을 이해하려면 사람의 검토가 필수입니다.
다음 검토 체크리스트를 사용하세요:
- 자산 이전, 역할 변경, 코드를 업그레이드, 활동 일시 정지 또는 신뢰할 수 있는 주소 변경을 하는 모든 함수를 나열하세요.
- 각 함수의 의도된 호출자와 권한 부여 규칙을 파악하세요.
- 그 권한을 부여, 철회, 이전 또는 초기화할 수 있는 모든 함수를 역추적하세요.
- 권한이 부여된 내부 함수를 호출하는 외부 래퍼를 점검하세요.
- 초기화는 한 번만 수행 가능하며, 배포 과정 하에서만 일어나도록 검증하세요.
- 비상 역할이 관련된 일반 작업 또는 거버넌스 작업을 수행할 수 없음을 확인하세요.
- 역할 철회, 소유권 이전, 제로 주소 처리 시험을 진행하세요.
- 프록시, 상속된 계약, delegatecall 경로도 동일한 권한 경계의 일부로 검토하세요.
Slither는 누락된 권한 부여 패턴, 위험한 tx.origin 사용, 그리고 일관성 없는 수정자를 표시할 수 있지만, 그 결과는 수동 검증이 필요합니다. Foundry의 퍼징은 임의 주소에서 호출을 생성하고, 권한이 없는 호출자가 권한이 필요한 상태를 변경하지 못하는지 검증하는 데 유용합니다.
무결성(invariant) 테스트가 특히 효과적입니다. 예를 들어, 금고의 무결성은 pay를 통해 지급 역할이 없는 주소가 금고 잔액을 줄일 수 없음을 선언할 수 있습니다. 또 다른 무결성은 오직 소유자만 역할 구성원을 변경할 수 있다는 규칙을 검증하는 것입니다. 이러한 속성들은 예상 사용자 흐름만이 아니라, 보안 규칙 그 자체를 테스트하는 방식입니다.
보안 인사이트, Soken 방법론: 권한 검증을 수정자 목록으로서가 아니라, 능력(capability) 그래프로서 검토하세요. 모든 권한 부여 결과는 의도적이고 감사 가능해야 하며, 모든 의도하지 않은 경로는 잠재적 권한 상승 경로입니다.
손상된 접근 제어 공격은 권한을 명확히 하고, 각 역할의 권한 범위를 최소화하며, 해당 권한을 부여하는 함수를 자금 이동 함수만큼 신중하게 보호함으로써 방지됩니다. 누구든지 인출자(Withdrawer)를 지정할 수 있다면, 올바르게 제한된 인출 함수라도 안전하지 않습니다.
배포된 계약과 계획된 계약에 대한 권한 맵을 만들어 각 규칙을 수정자와 무결성 검증 조건으로 전환하는 것으로 시작하세요. 더 깊은 Solidity 보안 연구와 유사 취약점 클래스에 대해서는 Soken Hub를 참조하십시오.