Solidity에서 깨진 Access Control 공격 방지 방법

작성자

손상된 접근 제어 공격은 Solidity 계약이 자산 이동, 구현 변경, 역할 할당 또는 신뢰할 수 있는 주소 구성과 같이 제한되어야 하는 상태 변경 능력에 대해 신뢰할 수 없는 호출자가 접근하도록 허용할 때 발생합니다. 공격자는 암호학을 깨뜨릴 필요가 없으며, 오히려 누락되거나 잘못된 또는 지나치게 포괄적인 권한 검사 기능을 이용하여 실질적인 권한 상승을 이끕니다.

이 영향은 프로토콜 전체에 영향을 미칠 수 있는데, 권한이 부여된 함수들이 종종 풀링된 자금이나 거버넌스에 중요한 구성요소를 제어하기 때문입니다. Poly Network는 2021년 8월 10일에 Ethereum, BSC, Polygon에서 6억 1천 1백만 달러를 잃었으며, 이는 DefiLlama가 부적절한 접근 제어로 분류한 사건입니다. 따라서 권한 부여는 UI 문제 또는 행정적 세부 사항이 아니며, 계약의 핵심 보안 범위의 일부입니다.

Solidity에서 손상된 접근 제어 취약점이란 무엇인가?

손상된 접근 제어 취약점은 계약이 msg.sender의 권한을 신뢰성 있게 검증하지 않거나, 권한 부여 모델이 해당 작업에 필요 이상으로 많은 권한을 부여할 때 존재합니다. Solidity에서는 이 실패가 주로 민감한 저장소를 변경하거나, 자산을 전송하거나, 코드를 업그레이드하거나, 신뢰할 수 있는 상대방을 변경하는 외부 호출 가능 함수에서 나타납니다.

계약이 owner 변수만 있더라도 취약할 수 있습니다. 중요한 점은 모든 권한이 부여된 실행 경로가 helper 함수, 초기화 함수, 역할 관리 함수, 업그레이드 훅 등을 포함하여 해당 권한을 올바르게 검증하는지 여부입니다.

일반적인 손상된 접근 제어 공격은 다음 순서를 따릅니다:

  1. 공격자는 권한이 부여된 영향을 미치는 외부 호출 가능 함수를 식별합니다.
  2. 이 함수는 호출자 검증이 없거나 잘못된 주소를 검증하거나, 공격자가 제어하는 상태에 의존합니다.
  3. 공격자는 프론트엔드 제한을 우회하여 직접 함수를 호출합니다.
  4. 이 호출은 역할을 부여하거나 신뢰할 수 있는 주소를 변경하거나 자산 이동 작업을 호출합니다.
  5. 공격자는 새롭게 확보된 능력을 이용해 자금을 빼내거나 프로토콜 동작을 변경합니다.
  6. 계약은 이더리움 가상 머신(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

이 사고들은 공통된 보안 교훈을 전달합니다. 즉, 계약이 의도한 신뢰 모델과 일치하지 않는 권한 경계로 인해 프로토콜의 경제적 가치가 노출될 수 있다는 점입니다. 피해를 입은 구체적인 함수는 다를 수 있지만, 실패 유형은 같습니다: 설계가 부여하지 말았어야 할 권한이 누군가에게 부여되거나 행사되는 경우입니다.

배포 전 손상된 접근 제어 취약점을 어떻게 탐지할 수 있나?

접근 제어 취약점 탐지는 민감한 상태 변화와 그 승인 조건 사이의 맵핑을 하고, 그 조건을 우회할 수 있는 대체 호출 경로가 존재하지 않음을 증명하는 과정을 포함합니다. 자동화 도구는 의심스러운 패턴을 식별하는 데 도움을 주지만, 실질적 권한 부여, 신뢰 가정, 권한 상승 체인을 이해하려면 사람의 검토가 필수입니다.

다음 검토 체크리스트를 사용하세요:

  1. 자산 이전, 역할 변경, 코드를 업그레이드, 활동 일시 정지 또는 신뢰할 수 있는 주소 변경을 하는 모든 함수를 나열하세요.
  2. 각 함수의 의도된 호출자와 권한 부여 규칙을 파악하세요.
  3. 그 권한을 부여, 철회, 이전 또는 초기화할 수 있는 모든 함수를 역추적하세요.
  4. 권한이 부여된 내부 함수를 호출하는 외부 래퍼를 점검하세요.
  5. 초기화는 한 번만 수행 가능하며, 배포 과정 하에서만 일어나도록 검증하세요.
  6. 비상 역할이 관련된 일반 작업 또는 거버넌스 작업을 수행할 수 없음을 확인하세요.
  7. 역할 철회, 소유권 이전, 제로 주소 처리 시험을 진행하세요.
  8. 프록시, 상속된 계약, delegatecall 경로도 동일한 권한 경계의 일부로 검토하세요.

Slither는 누락된 권한 부여 패턴, 위험한 tx.origin 사용, 그리고 일관성 없는 수정자를 표시할 수 있지만, 그 결과는 수동 검증이 필요합니다. Foundry의 퍼징은 임의 주소에서 호출을 생성하고, 권한이 없는 호출자가 권한이 필요한 상태를 변경하지 못하는지 검증하는 데 유용합니다.

무결성(invariant) 테스트가 특히 효과적입니다. 예를 들어, 금고의 무결성은 pay를 통해 지급 역할이 없는 주소가 금고 잔액을 줄일 수 없음을 선언할 수 있습니다. 또 다른 무결성은 오직 소유자만 역할 구성원을 변경할 수 있다는 규칙을 검증하는 것입니다. 이러한 속성들은 예상 사용자 흐름만이 아니라, 보안 규칙 그 자체를 테스트하는 방식입니다.

보안 인사이트, Soken 방법론: 권한 검증을 수정자 목록으로서가 아니라, 능력(capability) 그래프로서 검토하세요. 모든 권한 부여 결과는 의도적이고 감사 가능해야 하며, 모든 의도하지 않은 경로는 잠재적 권한 상승 경로입니다.

손상된 접근 제어 공격은 권한을 명확히 하고, 각 역할의 권한 범위를 최소화하며, 해당 권한을 부여하는 함수를 자금 이동 함수만큼 신중하게 보호함으로써 방지됩니다. 누구든지 인출자(Withdrawer)를 지정할 수 있다면, 올바르게 제한된 인출 함수라도 안전하지 않습니다.

배포된 계약과 계획된 계약에 대한 권한 맵을 만들어 각 규칙을 수정자와 무결성 검증 조건으로 전환하는 것으로 시작하세요. 더 깊은 Solidity 보안 연구와 유사 취약점 클래스에 대해서는 Soken Hub를 참조하십시오.

작성자

자주 묻는 질문

Solidity의 broken access control 취약점이 무엇인가요?

이 취약점은 contract가 msg.sender의 권한 검증에 실패할 때 발생하며, 믿을 수 없는 호출자가 권한이 필요한 상태 변경을 수행할 수 있어 cryptography를 깨뜨리지 않고도 권한 상승이 가능하게 만듭니다.

Broken access control 공격이 privilege escalation을 어떻게 유발하나요?

이 공격은 권한이 있는 사용자에게만 허용된 capability를 untrusted 호출자가 접근하게 허용하여, 자산 이동, 역할 변경, 구현 변경 등 상태 변경이 가능하게 하며 권한 상승을 유발합니다.

어떤 Solidity 액션에 access control이 필요하나요?

모든 상태 변경 capability에 대해 access control이 필요하며, 예를 들어 자산 이동, 계약 구현 변경, 역할 할당, 신뢰할 수 있는 주소 구성 등이 해당됩니다. 각 capability는 msg.sender에 대한 신뢰성 있는 권한 결정이 필요합니다.

Poly Network의 access control 사건이 무엇인가요?

2021-08-10, Poly Network는 Ethereum, BSC, Polygon에서 6억 1100만 달러를 잃었습니다. 이 사건은 권한 검증이 왜 관리뿐만 아니라 pooled funds와 거버넌스 중요한 설정을 보호하는지 보여줍니다.

권한 부여는 왜 Solidity 보안의 핵심 경계인가요?

권한 부여는 매우 중요하며, 권한이 필요한 함수는 pooled funds와 거버넌스 중요한 설정을 제어하기 때문입니다. 잘못된 또는 과도한 권한 검사는 신뢰할 수 없는 호출자가 제한되어야 할 기능에 접근하도록 하여, 보안 위험을 내포합니다.

채팅