개발자가 놓치기 쉬운 솔리디티(Solidity) 보안 코딩 규칙 10선

개발자가 놓치기 쉬운 솔리디티(Solidity) 보안 코딩 규칙 10선 관련 이미지
안녕하세요, 10년 차 생활 블로거 김창수입니다. 요즘 코인 시장이 다시 들썩이면서 스마트 컨트랙트 개발에 도전하는 분들이 정말 많아졌더라고요. 저도 예전에 호기심에 솔리디티를 공부하며 작은 프로젝트를 배포했다가 식은땀을 흘렸던 기억이 나네요.
블록체인 세상은 수정이 불가능하다는 특성 때문에 단 하나의 작은 실수가 곧장 자산의 손실로 이어지는 무서운 곳이기도 하거든요. 일반적인 웹 개발과는 접근 방식 자체가 완전히 달라야 한다는 점을 뼈저리게 느꼈답니다. 오늘은 제가 직접 겪고 공부하며 정리한, 개발자가 정말 놓치기 쉬운 보안 수칙들을 공유해 보려고 해요.
완벽한 코드는 없겠지만, 적어도 남들이 다 당하는 보안 사고는 피해야 하지 않겠어요? 초보 개발자부터 숙련자까지 한 번쯤은 다시 점검해야 할 핵심 포인트들을 차근차근 짚어드릴게요.
1. 재진입 공격(Reentrancy)의 공포와 방어법
2. 정수 오버플로우와 최신 컴파일러의 변화
3. 권한 관리의 핵심: 누가 함수를 호출하는가
4. 가스 한도 초과로 인한 서비스 거부 공격
5. 주요 보안 취약점 비교 분석표
6. 솔리디티 보안 관련 자주 묻는 질문
재진입 공격(Reentrancy)의 공포와 방어법
솔리디티 보안을 이야기할 때 가장 먼저 나오는 단골손님이 바로 재진입 공격이거든요. 이 공격은 스마트 컨트랙트가 외부 컨트랙트와 상호작용할 때 발생하는데요. The DAO 해킹 사건으로 이더리움 생태계를 뒤흔들었던 주범이기도 하죠.
제가 처음에 실수했던 부분이 바로 이 지점이었어요. 사용자가 출금을 요청하면 잔액을 먼저 확인하고, 돈을 보낸 뒤에 잔액을 0으로 업데이트하는 순서로 코드를 짰거든요. 현실 세계에서는 당연한 순서 같지만, 블록체인에서는 돈을 보내는 순간 제어권이 상대방에게 넘어가버리더라고요. 상대방이 fallback 함수를 이용해 다시 출금 함수를 호출하면 잔액이 0이 되기 전에 계속 돈이 빠져나가는 무시무시한 일이 벌어집니다.
로직의 순서만 바꿔도 대부분의 공격을 막을 수 있다는 게 신기하지 않나요? 하지만 많은 개발자가 코딩의 흐름에만 집중하다 보니 이런 상호작용의 위험성을 간과하곤 하더라고요. 항상 External Call이 발생하는 지점을 유심히 관찰하는 습관을 들여야 합니다.
정수 오버플로우와 최신 컴파일러의 변화
두 번째로 주의해야 할 점은 정수 계산의 한계치입니다. 예전 버전의 솔리디티에서는 0에서 1을 빼면 엄청나게 큰 숫자로 변하는 언더플로우 현상이 발생했거든요. 이를 막기 위해 SafeMath 라이브러리를 주렁주렁 달고 다녔던 기억이 나네요.
다행히 솔리디티 0.8.0 버전부터는 컴파일러 차원에서 기본적으로 산술 연산 검사를 수행해 줍니다. 그래서 예전만큼 걱정할 필요는 없어졌지만, 여전히 unchecked 블록을 사용할 때는 주의가 필요해요. 가스비를 아끼겠다고 검사를 건너뛰다가 예상치 못한 보안 구멍이 뚫릴 수 있기 때문이죠.
특히 토큰 총 발행량이나 사용자 예치금 같은 중요한 수치를 다룰 때는 아무리 강조해도 지나치지 않더라고요. 작은 숫자의 오차가 전체 시스템의 신뢰를 무너뜨릴 수 있다는 점을 항상 명심해야 합니다.
주요 보안 취약점 비교 분석표
보안 취약점들은 각각 성격이 다르고 해결 방법도 차이가 있어요. 제가 공부하면서 헷갈렸던 부분들을 표로 정리해 보았는데, 한눈에 들어오실 거예요.
| 취약점 명칭 | 주요 원인 | 위험도 | 권장 해결책 |
|---|---|---|---|
| 재진입(Reentrancy) | 외부 호출 시 제어권 상실 | 매우 높음 | Checks-Effects-Interactions 패턴 |
| 권한 오용(Access Control) | 제어자(Modifier) 누락 | 매우 높음 | Ownable, Role-based Access |
| 가스 한도 초과(DoS) | 무한 루프 혹은 거대 배열 순회 | 보통 | Pull-over-Push 패턴 사용 |
| 랜덤성 조작 | 블록 해시 등 온체인 데이터 사용 | 높음 | Chainlink VRF 같은 오라클 활용 |
| 가시성 설정 오류 | 함수 가시성(public/private) 미지정 | 보통 | 명시적 가시성 지정 의무화 |
권한 관리의 핵심: 누가 함수를 호출하는가
스마트 컨트랙트에서 가장 황당한 사고는 관리자만 실행해야 하는 함수를 누구나 실행할 수 있을 때 발생하더라고요. 예를 들어 selfdestruct 함수나 자금을 인출하는 함수에 onlyOwner 같은 제어자가 빠져 있다면 어떻게 될까요? 생각만 해도 아찔하죠.
제가 예전에 테스트넷에서 배포했던 컨트랙트가 하나 있었는데, 관리자 변경 함수를 깜빡하고 public으로 열어둔 적이 있었어요. 누군가 제 컨트랙트의 주인을 본인으로 바꿔버리는 바람에 테스트 자산을 몽땅 잃어버리는 실패를 경험했답니다. 실제 메인넷이었다면 정말 상상도 하기 싫은 일이죠.
그래서 저는 이제 모든 함수를 작성할 때 "이 함수를 호출할 수 있는 사람은 누구인가?"를 가장 먼저 자문해 보곤 합니다. msg.sender를 직접 체크하기보다는 검증된 라이브러리의 제어자를 사용하는 게 훨씬 안전하다는 것도 깨달았고요. 권한 분리는 보안의 기본 중의 기본임을 잊지 마세요.
가스 한도 초과로 인한 서비스 거부 공격
이더리움 네트워크에는 한 블록에 담을 수 있는 연산량의 한계인 '가스 리밋'이 존재하거든요. 만약 여러분의 컨트랙트가 무한히 늘어날 수 있는 배열을 반복문으로 돌리고 있다면, 언젠가 가스 부족으로 아예 작동을 멈춰버릴 수도 있습니다.
공격자는 이를 악용해 일부러 배열의 크기를 키우거나, 가스 소모가 많은 작업을 유도해 컨트랙트를 마비시키기도 하더라고요. 특히 투표 시스템이나 다수의 사용자에게 이자를 지급하는 로직에서 이런 문제가 자주 발생하곤 합니다.
반복문을 쓸 때는 항상 탈출 조건과 최대 반복 횟수를 고민해야 하더라고요. 데이터가 수만 건으로 늘어나도 이 코드가 돌아갈 것인지 미리 시뮬레이션해 보는 습관이 중요합니다. 가스비는 사용자에게만 부담이 되는 게 아니라, 서비스의 생사까지 결정짓는 중요한 요소니까요.
자주 묻는 질문
Q. 솔리디티 보안 감사는 필수인가요?
A. 메인넷에 큰 자금을 다루는 컨트랙트를 올린다면 무조건 필수라고 생각해요. 개발자가 자기 코드의 허점을 찾는 건 정말 어렵거든요.
Q. tx.origin 대신 msg.sender를 써야 하는 이유는 뭔가요?
A. tx.origin은 최초 호출자를 가리키기 때문에 피싱 공격에 취약하거든요. 중간에 가짜 컨트랙트를 끼워 넣어 권한을 탈취당할 수 있어서 msg.sender 사용을 권장합니다.
Q. 스마트 컨트랙트를 한 번 배포하면 절대 수정할 수 없나요?
A. 기본적으로는 그렇지만, 프록시(Proxy) 패턴을 사용하면 로직 컨트랙트를 교체하는 방식으로 업그레이드가 가능하더라고요. 하지만 구조가 복잡해지니 신중해야 합니다.
Q. Private 변수는 남들이 절대 못 보나요?
A. 아니요, 블록체인상의 모든 데이터는 공개되어 있어요. Private은 컨트랙트 코드 내에서만 접근 가능하다는 뜻이지, 데이터를 숨겨주는 기능이 아니랍니다.
Q. 보안 툴 중에 추천할 만한 게 있을까요?
A. Slither나 Mythril 같은 정적 분석 도구를 추천드려요. 코드를 올리기 전에 돌려보면 기본적인 실수들은 대부분 잡아내 주더라고요.
Q. transfer() 대신 call()을 쓰는 이유가 뭔가요?
A. transfer는 가스 제한이 2300으로 고정되어 있어서 미래에 가스비 정책이 바뀌면 작동하지 않을 위험이 있거든요. 요즘은 call을 쓰고 재진입 방지를 하는 게 정석처럼 굳어지는 추세예요.
Q. 랜덤 숫자가 필요한데 어떻게 해야 하나요?
A. 블록 해시나 타임스탬프는 채굴자가 조작할 수 있어서 위험해요. Chainlink VRF 같은 외부 오라클을 사용하는 게 가장 안전한 방법이더라고요.
Q. 시간 제한 기능을 넣을 때 주의할 점은?
A. block.timestamp는 네트워크 상황에 따라 몇 초 정도 오차가 있을 수 있다는 점을 감안해야 합니다. 아주 정밀한 시간이 필요한 로직이라면 주의가 필요하거든요.
Q. 컨트랙트 용량 제한도 있나요?
A. 네, 이더리움은 컨트랙트 하나당 약 24KB의 크기 제한이 있어요. 기능이 너무 많으면 컨트랙트를 쪼개거나 라이브러리를 활용해야 하더라고요.
솔리디티 개발은 참 매력적이지만, 그만큼 책임감이 따르는 작업이라는 걸 매번 느껴요. 제가 오늘 정리해 드린 내용들이 여러분의 소중한 프로젝트를 지키는 데 조금이나마 도움이 되었으면 좋겠네요. 완벽한 보안은 없지만, 끊임없이 의심하고 검증하는 자세가 우리를 더 나은 개발자로 만들어줄 거라 믿습니다.
혹시 더 궁금한 점이나 본인만의 보안 꿀팁이 있다면 댓글로 자유롭게 남겨주세요. 저도 여러분의 경험을 배우고 싶거든요. 늘 안전하고 즐거운 코딩 생활 되시길 응원하겠습니다!
작성자: 김창수
10년 차 IT 생활 블로거이자 스마트 컨트랙트 연구가입니다. 복잡한 기술을 일상의 언어로 풀어내는 것을 좋아합니다.
본 포스팅은 정보 제공을 목적으로 하며, 실제 개발 시에는 반드시 공식 문서와 최신 보안 가이드를 참조하시기 바랍니다. 코드의 결함으로 인한 손실에 대해 필자는 책임을 지지 않습니다.
댓글
댓글 쓰기