https://github.com/terning-farewell-thanks/terning-farewell-server/pull/17
새로운 API 기능을 구현하고 코드 리뷰를 받게 되었습니다. 인증 코드를 생성하는 간단한 로직이었고, 테스트 역시 모두 통과했기에 큰 문제 없이 마무리될 것이라 생각했습니다.
하지만 리뷰 과정에서 제가 미처 생각하지 못했던 부분에 대한 피드백을 받았습니다.
"혹시
java.util.Random
을 사용하신 특별한 이유가 있으신가요? 보안 관련 이슈가 있을 수 있어 보입니다."
피드백을 받고 처음에는 의문이 들었습니다. 기능적으로 오류가 없고 정상적으로 동작하는데 어떤 위험이 있을까 하고 단순하게 생각했죠. 저는 그저 java.util
패키지에 있는 기본적인 클래스를 사용했을 뿐이었습니다.
이번 코드 리뷰는 제가 java.util.Random
의 동작 방식을 더 깊이 공부하고, SecureRandom
의 필요성을 깨닫는 소중한 학습의 계기가 되었습니다.
java.util.Random
의 동작 방식처음에는 Random
과 SecureRandom
의 차이를 성능 정도로만 막연히 생각했습니다. 하지만 피드백을 계기로 관련 내용을 찾아보면서 제 코드에 어떤 잠재적 위험이 있는지 알게 되었습니다.
java.util.Random
은 진짜 난수가 아닌 의사 난수(Pseudorandom)를 생성합니다. 즉, 시작점인 시드(seed) 값이 정해지면 그 뒤에 나오는 숫자들은 정해진 순서에 따라 생성된다는 특징이 있었습니다.
가장 큰 문제는 제가 new Random()
처럼 시드를 따로 지정하지 않았을 때, 현재 시스템 시간이 시드 값으로 사용된다는 점이었습니다. 만약 공격자가 인증 코드가 생성된 시간을 어느 정도 예측할 수 있다면, 비슷한 시간대의 시드 값을 모두 시도하여 난수 패턴 전체를 알아낼 수 있었습니다. 이는 앞으로 생성될 인증 번호까지 예측 가능하다는 것을 의미했고, 심각한 문제로 이어질 수 있었습니다.
단순히 시드 값의 문제만은 아니었습니다. Random
이 사용하는 선형 합동 생성기(LCG) 알고리즘은 내부 상태가 48비트로 비교적 작아서, 생성된 난수 값 몇 개만 확보하면 전체 패턴을 역추적하는 공격이 가능했습니다.
제 코드가 기능적 버그는 아니었지만, 이처럼 공격자에게 예측의 빌미를 제공할 수 있다는 점에서 '보안 취약점'으로 분류될 수 있다는 것을 배우게 되었습니다.
java.security.SecureRandom
이 문제를 해결할 방법을 찾으며 java.security.SecureRandom
에 대해 알게 되었습니다. 이 클래스는 암호학적으로 안전한 의사 난수 생성기(CSPRNG)로, 이름처럼 보안을 목적으로 설계되었습니다. 핵심은 다음과 같았습니다.
"이전에 생성된 값을 알더라도 다음 값을 예측하는 것이 계산적으로 거의 불가능하다."
SecureRandom
은 Random
과 달리, 예측하기 어려운 시스템의 여러 상태 값, 즉 엔트로피(Entropy)를 수집하여 난수를 생성했습니다.
애플리케이션 레벨의 난수 생성이 운영체제의 깊은 부분과 연결되어 안전성을 보장받는다는 점이 인상 깊었습니다.
학습한 내용을 바탕으로 기존 코드를 수정했습니다.
// 경고: 이 코드는 예측 가능성에 대한 보안 이슈가 있습니다.
import java.util.Random;
public class InsecureAuthCode {
public static String generateCode() {
Random rnd = new Random();
int number = rnd.nextInt(999999);
return String.format("%06d", number);
}
}
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class AuthCodeGenerator {
public static String generateSecureCode() throws NoSuchAlgorithmException {
// 1. 보안 강도가 높은 CSPRNG 인스턴스를 가져옵니다.
SecureRandom secureRandom = SecureRandom.getInstanceStrong();
// 2. 예측 불가능한 정수를 생성합니다.
int randomInt = secureRandom.nextInt(1000000);
// 3. 6자리 형식을 보장하도록 포맷팅합니다.
return String.format("%06d", randomInt);
}
}
new Random()
을 SecureRandom.getInstanceStrong()
으로 변경하는 간단한 수정이었지만, 코드의 보안 수준은 크게 향상되었습니다. 이 변화로 예측 가능성이라는 잠재적 위험을 제거할 수 있었습니다.
Random
과 SecureRandom
의 주요 차이점이번 기회에 두 클래스의 차이점을 다음과 같이 정리할 수 있었습니다.
기능 | java.util.Random | java.security.SecureRandom |
---|---|---|
주요 목적 | 시뮬레이션, 테스트 등 비보안 작업 | 토큰, 키, 비밀번호 등 보안이 중요한 작업 |
예측 가능성 | 높음 (보안 목적으로 부적합) | 계산적으로 예측 불가능 |
시드 소스 | 결정론적 (주로 시스템 시간) | 비결정론적 (OS 엔트로피) |
성능 | 매우 빠름 | 상대적으로 느림 |
SecureRandom
이 더 안전하다면 왜 Random
도 계속 사용될까 하는 궁금증이 생겼습니다. 가장 큰 이유는 역시 성능이었습니다. SecureRandom
은 엔트로피를 수집하는 과정 때문에 Random
보다 훨씬 느립니다.
하지만 이 성능 차이가 실제 애플리케이션에 미치는 영향을 생각해 보았습니다. 인증 코드나 세션 ID를 하나 생성하는 데 걸리는 시간은 밀리초보다 훨씬 작은 단위였습니다. 이는 네트워크 지연이나 데이터베이스 조회 시간에 비하면 거의 무시할 수 있는 수준이었죠.
따라서 보안이 중요한 대부분의 상황에서는 SecureRandom
으로 인한 성능 저하를 걱정하기보다, 예측 가능성으로 인한 보안 위협을 막는 것이 훨씬 중요하다고 판단했습니다. 보안을 위해 감수해야 할 합리적인 비용이라고 생각하게 되었습니다.
이번 코드 리뷰는 제게 아주 소중한 배움의 기회였습니다. 기능이 '단순히 동작하는 것'을 넘어 '안전하게 동작하는 것'의 중요성을 다시 한번 깨닫게 되었습니다.
이번 학습을 통해 정리한 내용을 공유하며 글을 마칩니다.
java.util.Random
은 예측 가능하므로, 보안이 조금이라도 중요한 곳에서는 사용을 지양해야 합니다.java.security.SecureRandom
으로 생성하는 것이 바람직합니다.SecureRandom
의 성능은 대부분의 웹 애플리케이션 환경에서 보안을 위해 충분히 감수할 수 있는 수준입니다.작은 코드 한 줄도 시스템 전체의 안정성에 영향을 줄 수 있다는 사실을 되새기며, 앞으로는 기능 구현뿐만 아니라 잠재적인 위험 요소까지 고려하는 개발자가 되기 위해 노력해야겠습니다.