모듈로 편향이 있는 암호학적 난수

Biased Cryptographic Randomness (Modulo Bias)

설명

암호화 알고리즘을 통해 생성한 안전한 난수에서 나온 값을 단순히 % (모듈로) 연산 등으로 특정 범위로 줄이면, 범위가 원래 난수 공간으로 딱 나누어떨어지지 않는 경우 값의 분포가 고르지 않게 됩니다. 이를 modulo bias라고 하며, 일부 값이 다른 값보다 더 자주 나오게 됩니다. 공격자는 이 bias로 토큰·코드·식별자의 예측 난이도를 낮추어 추측(브루트포스)의 성공 확률을 높이거나, 충돌/중복을 유도하여 보안을 약화시킬 수 있습니다.

잠재적 영향

  • 인증/토큰 예측 가능성 증가: 패스워드 재설정 토큰, 세션 ID, OTP 등에서 특정 값이 더 자주 나와 추측 성공률이 상승합니다.
  • 추측 확률의 불균형: 가능한 값이 사라지는 것은 아니지만, 더 자주 나오는 값을 먼저 추측하면 성공 확률이 높아질 수 있습니다.
  • 인증 우회/권한 상승: 예측 가능한 CSRF 토큰·초대 코드·가입 코드 등을 노려 계정 탈취나 권한 오용이 발생할 수 있습니다.
  • 충돌 및 중복 증가: 인증 코드나 쿠폰 코드의 중복 발생 가능성이 높아져 재사용·사기·서비스 남용 위험이 커집니다.

해결 방법

  • 암호학적 난수에 직접 % (모듈로) 등 축소 연산을 적용하지 마세요.
  • Node.js에서는 crypto.randomInt()처럼 편향을 제거한 API를 사용하세요.
  • 반드시 수학적으로 범위를 줄여야 한다면 거절 표본추출(rejection sampling)을 사용해 균등 분포 범위 밖의 값을 버리세요.
  • 문자열 토큰은 검증된 라이브러리(예: crypto-random-string) 또는 충분히 큰 바이트를 그대로 안전한 인코딩(hex/base64url 등)으로 변환해 사용하세요.

예시

변경 전

javascript
// 비준수: 모듈로 바이어스로 bias된 6자리 코드 생성
const crypto = require("crypto");

function generateOtpBad() {
  // 0..999999 범위로 줄일 때 % 사용 -> bias 발생
  const raw = crypto.randomBytes(4).readUInt32BE();
  const n = raw % 1_000_000; // 취약: 균등하지 않음
  return n.toString().padStart(6, "0");
}

console.log(generateOtpBad());

변경 후

javascript
// 준수: bias 제거 API 또는 거절 표본추출 사용
const crypto = require("crypto");

// 1) 권장: Node.js 내장 bias 제거 API
function generateOtpSafe() {
  const n = crypto.randomInt(0, 1_000_000); // 상한 미포함, 균등 분포 보장
  return n.toString().padStart(6, "0");
}

// 2) 대안: 거절 표본추출(rejection sampling)
function generateOtpSafeLegacy() {
  const bound = 1_000_000;
  const max = Math.floor(0x1_0000_0000 / bound) * bound; // 2^32에서 bound로 나누어떨어지는 최대값
  while (true) {
    const r = crypto.randomBytes(4).readUInt32BE();
    if (r < max) {
      return (r % bound).toString().padStart(6, "0");
    }
  }
}

console.log(generateOtpSafe());

설명:

  • 변경 전: 난수(0..2^32-1)를 1,000,000으로 나눈 나머지를 쓰면, 2^32가 1,000,000으로 나누어떨어지지 않기 때문에 일부 값이 더 자주 나옵니다. 이 bias은 공격자가 특정 코드/토큰을 더 높은 확률로 맞출 수 있게 만들어 예측 가능성을 키웁니다.
  • 변경 후: crypto.randomInt()는 내부적으로 bias을 제거하여 지정 범위 내 균등 분포를 보장합니다. 거절 표본추출 방식은 범위를 균등하게 나눌 수 있는 상한(max) 밖의 난수는 버린 뒤 나머지를 취하여 동일한 확률로 값을 생성하므로 bias이 제거됩니다.

참조