시냅스

Road to Web3 (18) 스마트컨트랙트 보안 기본 1: 재진입, 승인 allowance 사고 유형 본문

Road To Web3/Blockchain

Road to Web3 (18) 스마트컨트랙트 보안 기본 1: 재진입, 승인 allowance 사고 유형

ted k 2026. 1. 3. 22:13
이 글은 Ethereum 및 EVM 계열을 기준으로 설명합니다.

 

이번 편의 목표는 한 문장입니다.

  • 스마트컨트랙트 보안 사고는 코드 버그라기보다 권한과 상태 변경 순서의 설계 실패로 시작한다

이 글은 두 가지 전형을 먼저 익히는 데 집중합니다.

  • 재진입 reentrancy
  • 승인 allowance 기반 토큰 탈취

 

제가 처음 스마트컨트랙트를 접했을 때 인상은 이랬습니다.

  • 컨트랙트는 서버 코드처럼 한 번만 실행된다
  • 토큰은 송금이니까 잘못 보내면 끝이고, 승인은 별거 아니다
  • 보안은 감사 업체가 잡아줄 영역이다

현실은 이렇습니다.

  • 컨트랙트 호출은 외부 컨트랙트로 다시 튈 수 있다
  • 승인 allowance는 장기 권한 위임이고, 실수하면 자동이체 권한을 넘겨준 것과 같다
  • 한번 배포된 코드는 운영자가 바로 고치기 어렵다

web2로 번역하면

  • 재진입은 콜백 기반 결제 승인 흐름에서 순서가 뒤집히는 버그와 닮아 있다
  • allowance는 결제수단 토큰화의 정기결제 권한 위임과 닮아 있다

 

0) 위협 모델을 한 번만 고정하자

보안 이야기는 위협 모델이 없으면 끝없이 흔들립니다. 최소한 이 정도는 전제로 둡니다.

  • 내 컨트랙트는 임의의 외부 컨트랙트를 호출할 수 있다
  • 외부 컨트랙트는 악의적일 수 있다
  • 트랜잭션은 원자적이지만, 호출 체인은 깊어질 수 있다
  • 실패하면 상태는 롤백되지만 가스는 소모된다

 

1) 재진입 reentrancy: 상태를 갱신하기 전에 돈부터 보내면 생긴다

1.1 재진입이란

내 컨트랙트가 외부로 자금을 보내거나 외부 컨트랙트를 호출했을 때
상대가 다시 내 컨트랙트를 호출해 들어오는 상황입니다.

핵심은 이것입니다.

  • 외부 호출은 내 코드 흐름을 잠시 멈추고 상대에게 제어권을 준다

그래서 다음 순서가 위험합니다.

  • 송금 또는 외부 호출
  • 그 다음에 내부 상태 갱신

이 순서는 web2에서 말하면

  • DB 커밋 전에 외부 시스템에 성공 응답을 먼저 주는 것

과 비슷합니다.

 

1.2 취약한 패턴

아래는 의도적으로 단순화한 예시입니다.

// 취약 예시: 외부 전송 후 상태 갱신
function withdraw(uint256 amount) external {
    require(balance[msg.sender] >= amount);
    (bool ok,) = msg.sender.call{value: amount}();
    require(ok);
    balance[msg.sender] -= amount;
}

공격 컨트랙트는 콜백에서 다시 withdraw를 호출해, balance가 줄기 전에 여러 번 인출을 시도할 수 있습니다.

1.3 방어 원칙 3가지

원칙 1: 체크 후 상태 갱신 후 상호작용

check effects interactions 패턴

function withdraw(uint256 amount) external {
    require(balance[msg.sender] >= amount);
    balance[msg.sender] -= amount;
    (bool ok,) = msg.sender.call{value: amount}();
    require(ok);
}

원칙 2: 재진입 가드

락을 두는 방식입니다.

bool locked;

modifier nonReentrant() {
    require(!locked);
    locked = true;
    _;
    locked = false;
}

원칙 3: 풀 기반 설계

외부로 바로 보내기보다 사용자가 스스로 가져가게 설계하는 방식이 더 안전해지는 경우가 많습니다.
다만 이것도 결국 상태 갱신 순서를 지키는 게 핵심입니다.

1.4 web2 관점에서 재진입을 어떻게 이해할까

재진입은 동시성 락 이슈라기보다

  • 콜백으로 제어 흐름이 역전되는 문제

에 가깝습니다.

예시

  • 결제 승인 콜백에서 동일 주문을 다시 결제 승인 처리하는 버그
  • 웹훅을 수신한 순간, 내 상태가 확정되기 전에 외부가 다시 내 API를 때리는 상황

그래서 web2에서 익숙한 방어도 그대로 유효합니다.

  • 상태 머신을 둔다
  • 멱등성 키를 둔다
  • 외부 호출은 마지막에 한다

 

2) 승인 allowance 사고: 자동이체 권한을 무심코 넘기는 문제

2.1 allowance란

ERC20에서 transferFrom을 가능하게 하는 권한입니다.

  • owner가 spender에게 한도를 부여한다
  • spender는 그 한도 안에서 owner의 토큰을 가져간다

이 모델은 디앱에서 너무 자주 쓰입니다.

  • DEX 스왑
  • 대출 담보 예치
  • 구독형 결제
  • 게임 아이템 민팅

그래서 사고도 많이 납니다.

2.2 전형적인 사고 유형 6개

유형 요약 web2 비유
무제한 승인 최대값 승인 후 방치 자동이체 한도를 무제한으로 설정
피싱 승인 UI가 속여 다른 spender에 승인 가짜 결제창이 다른 가맹점으로 결제수단 등록
승인 대상 업그레이드 spender가 프록시로 바뀌며 권한이 위험해짐 결제대행사가 계약상 다른 주체로 바뀌는 리스크
approve 변경 레이스 기존 값에서 새 값으로 바꿀 때 중간 상태가 공격 창구 한도 변경 중 중복 출금
permit 오남용 서명 기반 승인 메시지를 탈취해 승인 실행 서명으로 결제수단 등록 동의서를 위조 사용
권한 과다 필요한 범위를 넘어선 spender 권한 부여 업무 계정에 관리자 권한 부여 후 방치

2.3 approve 변경 레이스: 흔한 함정

approve를 0이 아닌 값에서 다른 0이 아닌 값으로 바로 바꾸면
중간에 spender가 이전 allowance로 먼저 가져가고, 새 allowance도 다시 가져가는 창이 생길 수 있다고 알려져 있습니다.

그래서 실무에서는 보통

  • 먼저 0으로 설정
  • 그 다음 새 값으로 설정

패턴을 사용하거나, increaseAllowance decreaseAllowance를 사용합니다.

2.4 제품 관점에서의 안전한 UX

사용자는 대부분 allowance를 돈처럼 이해하지 않고 권한으로 이해하지 않습니다.
따라서 제품은 다음을 명시해야 합니다.

  • 승인이라는 행위는 결제가 아니라 권한 위임이다
  • 승인 한도는 나중에 회수 revoke할 수 있다
  • 무제한 승인 여부를 사용자에게 선택지로 준다

권장 UX 요소

  • 승인 화면에 spender 주소와 의미를 표시
  • 승인 금액을 최소로 제안
  • 서비스 내에서 revoke 안내와 링크 제공
  • 위험군 주소 차단 또는 경고

 

3) 체크리스트

배포 전 10개

1) 외부 호출은 마지막에 둔다
2) 상태 갱신 순서를 점검한다
3) 재진입 가드를 필요한 곳에 둔다
4) 토큰 전송은 안전한 라이브러리 사용을 검토한다
5) approve 레이스 방어를 반영한다
6) spender 업그레이드 가능성을 문서화한다
7) 이벤트 로그로 핵심 상태 변화를 남긴다
8) 실패와 재시도를 상태 머신으로 설계한다
9) 권한을 최소화한다
10) 통합 테스트에서 악성 컨트랙트를 포함한다

운영 중 10개

1) allowance 관련 고객 문의를 대응 문서로 만든다
2) 승인 한도와 revoke 가이드를 제품에 포함한다
3) 이상 출금 패턴을 탐지한다
4) 주요 기능의 업그레이드 경로와 권한을 감사한다
5) 오라클과 외부 의존성 장애 시 동작을 정의한다
6) 사고 대응 시나리오를 준비한다 pause 권한 등
7) 프론트에서 서명 메시지 피싱 방어를 강화한다
8) RPC 장애와 재전송 정책을 정리한다
9) 모니터링 지표를 둔다 실패율, revert, 가스 급등
10) 사용자 교육 자료를 주기적으로 갱신한다


 

결론

  • 재진입은 외부 호출로 제어권이 넘어가면서 상태 갱신 순서가 무너지는 문제다
  • allowance는 토큰 이동이 아니라 권한 위임이며, 실수하면 장기 권한을 넘겨준다
  • 보안은 코드 한 줄보다 제품 UX와 운영 정책까지 포함한 설계 문제다
Comments