솔리디티(Solidity) 코드 작성 시 자주 발생하는 보안 실수

뒤엉킨 구리선과 녹슨 금속 퍼즐 조각 위에 놓인 깨진 돋보기의 사실적인 모습.

뒤엉킨 구리선과 녹슨 금속 퍼즐 조각 위에 놓인 깨진 돋보기의 사실적인 모습.

안녕하세요, 10년 차 생활 밀착형 정보 블로거 김창수입니다. 요즘 부업이나 재테크에 관심 있는 분들이 많아지면서 블록체인 개발인 솔리디티에 도전하시는 분들이 제 주변에도 꽤 늘어난 것 같더라고요. 저도 처음에는 단순히 코드 몇 줄 짜면 돈이 복사되는 줄 알고 덤볐다가 정말 큰코다친 적이 있었거든요.

스마트 컨트랙트는 한 번 배포하면 수정하기가 무척 어렵다는 특징이 있어요. 그래서 처음에 코드를 짤 때 보안 실수를 하면 그게 바로 자산 손실로 이어지는 무서운 결과를 초래하더라고요. 제가 직접 경험하며 머리 싸매고 공부했던 내용들을 토대로 초보자들이 가장 많이 하는 실수들을 정리해 보려고 해요.

재진입 공격(Reentrancy)의 위험성

솔리디티 보안에서 가장 유명하면서도 무서운 것이 바로 재진입 공격이에요. fallback 함수를 이용해 컨트랙트의 잔액을 몽땅 빼가는 방식인데, 저도 예전에 테스트넷에서 연습할 때 이 코드를 잘못 짰다가 가상 자산이 순식간에 사라지는 걸 보고 등골이 오싹해졌던 기억이 나네요.

보통 출금 기능을 만들 때 잔액을 확인하고 돈을 보낸 뒤에 잔액을 0으로 바꾸는 순서로 코딩하기 쉽거든요. 그런데 돈을 보내는 순간 상대방 컨트랙트의 함수가 다시 내 출금 함수를 호출해버리면, 아직 잔액이 0으로 업데이트되지 않은 상태라 계속 돈이 빠져나가게 되더라고요. Check-Effects-Interactions 패턴을 반드시 지켜야 하는 이유가 바로 여기에 있어요.

창수의 꿀팁: 외부 호출을 하기 전에 항상 내부 상태(잔액 등)를 먼저 변경하세요. OpenZeppelin에서 제공하는 ReentrancyGuard 라이브러리를 사용하면 nonReentrant 수식어 하나로 이 문제를 아주 편하게 해결할 수 있답니다.

오버플로우와 언더플로우 비교

숫자 계산에서도 정말 황당한 실수가 많이 발생하더라고요. 0에서 1을 뺐는데 갑자기 엄청나게 큰 숫자가 되어버리거나, 최대치에서 1을 더했는데 0이 되어버리는 현상이죠. 솔리디티 0.8 버전부터는 기본적으로 체크를 해주지만, 그 이전 버전을 쓰거나 unchecked 블록을 사용할 때는 여전히 주의가 필요해요.

제가 겪었던 실패담을 하나 들려드리자면, 예전에 토큰 전송 로직을 짤 때 사용자 잔액 확인을 대충 했다가 언더플로우가 발생해서 누구나 무한대로 토큰을 생성할 수 있는 보안 구멍을 만든 적이 있었어요. 다행히 배포 전이라 망정이지 실제 서비스였다면 정말 끔찍한 상황이었을 것 같아요.

구분 오버플로우 (Overflow) 언더플로우 (Underflow)
발생 원인 데이터 타입의 최대값을 초과할 때 데이터 타입의 최소값보다 작아질 때
결과값 0 또는 아주 작은 값으로 순환 최대값 또는 아주 큰 값으로 순환
보안 위협 토큰 발행량 조작 가능성 무한 잔액 생성 위험
해결 방법 Solidity 0.8+ 사용 또는 SafeMath Solidity 0.8+ 사용 또는 SafeMath

접근 제어 설정의 치명적 실수

함수의 가시성(Visibility) 설정도 정말 중요한 부분인 것 같아요. public이나 external로 설정된 함수는 누구나 호출할 수 있는데, 관리자만 실행해야 하는 중요한 함수를 실수로 공개해버리면 그날로 컨트랙트는 끝장나거든요. 저는 처음 공부할 때 모든 함수를 그냥 public으로 만드는 나쁜 습관이 있었는데 이게 정말 위험한 행동이었더라고요.

특히 생성자(Constructor) 함수 이름을 오타 냈을 때 일반 함수가 되어버려 누구나 소유권을 뺏어갈 수 있었던 과거의 취약점은 정말 유명하죠. 지금은 문법이 바뀌어서 그런 일은 드물지만, 여전히 onlyOwner 같은 제어 장치를 빼먹는 실수는 현업에서도 종종 발생한다고 하더라고요.

주의사항: 중요한 상태 변수를 변경하거나 자금을 이동시키는 함수는 반드시 접근 제어 로직이 포함되어 있는지 두 번, 세 번 확인해야 합니다. 테스트 코드 작성 시 관리자가 아닌 계정으로 호출했을 때 정상적으로 거절되는지 꼭 확인해 보세요.

가스 한도와 반복문의 함정

솔리디티 코딩을 할 때 일반적인 프로그래밍 언어와 가장 다른 점이 바로 가스비 개념인 것 같아요. 데이터가 무한히 늘어날 수 있는 배열을 반복문으로 돌리는 코드를 짰다가는 나중에 가스 한도 초과로 컨트랙트가 아예 먹통이 될 수도 있거든요. 사용자가 많아질수록 배열이 커지고, 결국 실행에 필요한 가스비가 블록 가스 한도를 넘어서버리는 상황이죠.

이런 실수를 피하려면 반복문 사용을 최소화하고, 가능한 한 인덱스를 직접 참조하거나 오프체인에서 데이터를 처리하는 방식을 고민해야 하더라고요. 저도 처음에는 편리함 때문에 for 문을 남발했다가 가스비가 폭등하는 걸 보고 깜짝 놀라서 로직을 전부 수정한 적이 있었답니다.

창수의 꿀팁: 배열의 길이를 미리 캐싱해서 사용하거나, 큰 배열을 순회해야 한다면 작업을 여러 번의 트랜잭션으로 나누어서 처리하는 '풀(Pull) 방식' 결제 시스템 등을 활용해 보세요. 가스 효율성도 보안만큼이나 중요하답니다.

자주 묻는 질문

Q. 솔리디티 0.8 버전 이상을 쓰면 SafeMath가 필요 없나요?

A. 네, 0.8 버전부터는 산술 연산 시 자동으로 오버플로우와 언더플로우를 체크해주기 때문에 별도의 라이브러리 없이도 안전하게 계산할 수 있어요.

Q. tx.origin과 msg.sender의 차이점은 무엇인가요?

A. tx.origin은 트랜잭션을 처음 시작한 지갑 주소를 나타내고, msg.sender는 현재 호출을 보낸 주소를 의미해요. 인증을 위해 tx.origin을 쓰면 피싱 공격에 취약해질 수 있으니 주의해야 해요.

Q. 컨트랙트에 이더(ETH)를 강제로 보낼 수 있는 방법이 있나요?

A. selfdestruct 함수를 사용하거나 채굴 보상 주소로 지정하면 fallback 함수 없이도 이더를 보낼 수 있어요. 그래서 address(this).balance에 의존하는 로직은 위험할 수 있답니다.

Q. transfer()와 call() 중 어떤 것을 권장하나요?

A. 최근에는 가스비 변동 이슈 때문에 transfer()보다는 call()을 사용하는 것이 권장되는 추세예요. 다만 call()은 재진입 공격 방어 로직을 직접 구현해야 한다는 점을 잊지 마세요.

Q. 외부 컨트랙트를 호출할 때 발생하는 문제는 무엇인가요?

A. 외부 컨트랙트가 악의적이거나 에러를 발생시키면 내 컨트랙트 실행도 중단될 수 있어요. 항상 반환값을 확인하고 적절한 예외 처리를 해줘야 해요.

Q. private 변수는 정말 아무도 못 보나요?

A. 아니요, 온체인 상의 모든 데이터는 공개되어 있어요. private은 단지 다른 컨트랙트가 접근하지 못하게 할 뿐이지, 블록체인 노드를 통해 누구나 읽을 수 있다는 점을 명심하세요.

Q. 스마트 컨트랙트 난수는 어떻게 생성하나요?

A. 블록 해시나 타임스탬프를 이용한 난수는 채굴자가 조작할 수 있어요. 진짜 안전한 난수가 필요하다면 Chainlink VRF 같은 오라클 서비스를 이용하는 것이 좋아요.

Q. delegatecall은 왜 위험한가요?

A. 호출된 대상의 코드가 내 컨트랙트의 컨텍스트(스토리지)에서 실행되기 때문에, 대상 코드가 내 데이터를 마음대로 조작할 수 있는 권한을 가지게 되기 때문이에요.

솔리디티 개발은 정말 매력적이지만 그만큼 책임감이 따르는 작업이라고 생각해요. 제가 말씀드린 이런 기본적인 보안 수칙들만 잘 지켜도 치명적인 사고의 80% 이상은 예방할 수 있거든요. 완벽한 코드는 없겠지만, 계속해서 공부하고 검증하는 태도가 무엇보다 중요한 것 같아요.

오늘 정리해드린 내용이 여러분의 안전한 컨트랙트 개발에 조금이나마 도움이 되었으면 좋겠네요. 실수를 두려워하기보다 그 실수에서 배우는 과정이 우리를 더 좋은 개발자로 만들어줄 테니까요. 긴 글 읽어주셔서 정말 감사드리고 항상 안전하게 코딩하시길 바랄게요.

작성자: 10년 차 생활 블로거 김창수 (IT 및 블록체인 보안 관심 연구가)

면책조항: 본 포스팅은 정보 전달을 목적으로 하며, 실제 코드 작성 시에는 반드시 전문적인 보안 오딧(Audit)을 받으시길 권장합니다. 코드 결함으로 인한 자산 손실에 대해 필자는 책임을 지지 않습니다.

댓글

이 블로그의 인기 게시물

AI 도구를 활용한 자동 보안 검사와 전문가 수동 감사의 결과 차이

NFT 프로젝트 신뢰도를 높이는 보안 감사 인증 마크의 효과

개인키 분실 시 발생하는 문제