본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev14

Math.random()으로 만든 id가 충돌해서 목록이 뒤섞인 이유

  • #Engineering Note
  • #JavaScript
  • #Security

문제 발생

임시 id를 만들려고 흔히 쓰이는 이 코드를 썼는데, 목록이 커지자 같은 id가 두 번 나와 React 리스트가 뒤섞였습니다.

const id = Math.random().toString(36).slice(2, 9);   // 7자

세션 토큰에도 비슷한 방식을 쓰고 있다는 걸 나중에 발견했습니다.

원인 분석

문제는 두 가지이고 심각도가 다릅니다.

충돌. 위 코드는 36진수 7자리이므로 경우의 수가 36⁷ ≈ 780억입니다. 커 보이지만 생일 문제 때문에 수만 개만 만들어도 충돌 확률이 무시할 수 없는 수준이 됩니다. 잘라 쓸수록 급격히 나빠집니다.

예측 가능성. Math.random()암호학적으로 안전하지 않습니다. 빠른 난수 생성이 목적이라 내부 상태를 추정할 수 있고, 출력 몇 개를 관찰하면 다음 값을 예측할 수 있는 구현이 존재합니다. 세션 토큰이나 비밀번호 재설정 링크에 쓰면 그 자체가 취약점입니다.

crypto.randomUUID()암호학적으로 안전한 난수 생성기로 v4 UUID 36자 문자열을 만듭니다. 2022년 3월부터 Baseline입니다.

해결 방안

  1. id가 필요하면 crypto.randomUUID()를 씁니다.
const id = crypto.randomUUID();
// "36b8f84d-df4e-4d49-b662-bcde71a8764f"
  1. 보안 컨텍스트가 필요하다는 점을 압니다. HTTPS(그리고 localhost)에서만 사용할 수 있습니다. http://로 접근하는 개발 서버에서는 crypto.randomUUID is not a function이 납니다 — 브라우저가 오래돼서가 아니라 컨텍스트 때문입니다.
  2. 길이나 형식이 달라야 하면 getRandomValues를 씁니다. 같은 안전한 난수원을 쓰되 형식만 바꿉니다.
const bytes = crypto.getRandomValues(new Uint8Array(16));
const token = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
  1. 잘라 쓰지 않습니다. UUID가 길다고 앞 8자만 쓰면 위의 충돌 문제로 되돌아갑니다. 짧은 식별자가 필요하면 DB의 시퀀스처럼 유일성을 보장하는 출처를 쓰는 편이 맞습니다.
  2. React의 key에는 데이터의 id를 씁니다. 렌더 중에 만든 난수 id는 리렌더마다 달라져 컴포넌트가 통째로 다시 만들어집니다. 서버가 준 id가 없다면 목록을 만들 때 한 번만 부여하고 상태에 보관합니다.
  3. DOM 속성용 id라면 React의 useId가 맞습니다. 서버와 클라이언트에서 같은 값이 나와야 하이드레이션이 어긋나지 않는데, 난수는 그 조건을 만족하지 못합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.