목차11개 항목
출처와 참고
web.dev의 「Learn Privacy」 강좌의 지문 채취 모듈을 바탕으로 재구성한 노트다.
지문 채취란
지문 채취(fingerprinting) 는 기기·브라우저 설정의 오래 변하지 않는 특성을 여러 개 모아, 다른 사용자와 구별되는 조합으로 한 사람을 식별하는 기법이다. 설치된 글꼴 하나도 사람을 가르는 단서가 된다.
쿠키와 다른 점은 보이지 않고 지울 수 없다는 것이다.
| 구분 | 쿠키 | 지문 |
|---|---|---|
| 사용자가 보고 지울 수 있나 | 있다(설정, DevTools) | 없다 |
| 근거 | 사이트가 심은 값 | 원래 있던 기기 특성 |
| 동의 | 배너·설정으로 통제 가능 | 대개 동의 없이 동작 |
사기 탐지처럼 정당한 용도도 있지만, 서드파티 쿠키가 막힌 뒤 사이트 간 추적의 대안으로 쓰이는 경우가 많다(서드파티 쿠키와 브라우저 추적 방지). 로그인한 사용자를 알아보는 것은 사용자가 동의하고 언제든 로그아웃할 수 있으므로, 지문보다 훨씬 존중하는 식별이다.
수동과 능동
| 방식 | 정보원 | 브라우저의 대응 |
|---|---|---|
| 수동(passive) | 요청마다 기본으로 오는 정보: User-Agent, Accept-Language, IP | 값을 동결·축소한다. 이 값에 기대는 코드는 깨질 수 있다 |
| 능동(active) | 스크립트가 직접 묻는 정보: 글꼴 목록, 화면 크기, 캔버스·WebGL 렌더링 결과, 오디오 처리 결과, 미디어 장치 목록 | API를 막거나, 권한을 요구하거나, 값을 흐린다 |
엔트로피
각 특성이 얼마나 사람을 잘 가르는지는 엔트로피(entropy), 비트 단위로 잰다.
- 값의 종류가 적은 특성(운영체제)은 엔트로피가 낮다.
- 값의 종류가 많은 특성(설치 글꼴 조합)은 높다.
- 지문 라이브러리는 여러 특성을 합쳐 하나의 해시로 만든다. 하나하나는 흔해도, 좋아하는 것 열 개를 나열하면 친구 사이에서 한 사람이 특정되는 것과 같다.
미디어 장치는 실제로 쓰지 않아도 장치 이름 목록만으로 정보가 된다. 캔버스에 같은 글자를 그려도 GPU·드라이버·안티에일리어싱 차이로 픽셀이 달라진다.
브라우저의 대응
| 방법 | 내용 | 예 |
|---|---|---|
| 구현하지 않기 | 정보를 너무 많이 드러내는 기기 API를 아예 만들지 않는다 | 일부 브라우저의 Web Bluetooth·Web NFC 미구현 |
| 권한 요구 | 명시적 허락을 받아야 쓴다. 엔트로피는 그대로지만 몰래 쓸 수 없게 된다 | 위치, 카메라, 마이크 |
| 값 흐리기 | 결과를 일부러 덜 정확하게 해 모든 사용자를 비슷하게 보이게 한다("군중 속에 숨기") | performance.now() 정밀도 낮추기(타이밍 공격 방어도 겸함), 사파리의 단순화된 시스템 정보 |
| 동결 | 수동 정보의 세부를 고정값으로 바꾼다 | UA의 OS·마이너 버전 고정 |
파이어폭스는 ETP가 알려진 지문 채취 스크립트를 막고, 별도의 지문 보호 모드에서는 더 많은 값을 흐린다.
User-Agent 축소
UA 문자열로 브라우저를 갈라 처리하는 코드가 오래 쌓이면서, 브라우저마다 다른 브라우저인 척하는 문자열이 됐다(첫 토큰 Mozilla/5.0은 의미가 없다). 이제 주요 브라우저는 UA를 동결·축소한다.
| 남는 정보 | 사라진 정보 |
|---|---|
| 브랜드, 주 버전, 플랫폼(대략) | 마이너·빌드 버전(크롬은 Chrome/<주버전>.0.0.0), 상세 OS 버전(macOS는 10.15.7로 고정), CPU 아키텍처, 기기 모델 |
UA를 아예 비우면 수많은 사이트가 깨지므로, 지우지 않고 정보를 줄이는 쪽을 택했다.
할 일
- 코드와 의존성에서
navigator.userAgent,User-Agent헤더를 쓰는 곳을 찾는다. - 브라우저 판별을 기능 감지로 바꾼다.
// 나쁨: 브라우저 이름으로 분기
if (/Safari/.test(navigator.userAgent)) { /* ... */ }
// 좋음: 기능이 있는지 묻는다
if ('share' in navigator) { /* Web Share 사용 */ }
if (CSS.supports('selector(:has(*))')) { /* ... */ }- 브랜드·주 버전·플랫폼만 쓴다면 대체로 안전하다.
- 베타·프리뷰 빌드나 DevTools의 UA 재정의로 축소된 UA에서 시험한다.
User-Agent Client Hints
UA를 대체하는 수단으로 크로미움 계열 브라우저는 Client Hints를 보낸다. 사파리·파이어폭스는 지원하지 않으므로 없을 때를 대비해야 한다.
| 헤더 | 내용 |
|---|---|
Sec-CH-UA | 브랜드와 주 버전 목록 |
Sec-CH-UA-Mobile | 모바일 여부(?1/?0) |
Sec-CH-UA-Platform | 운영체제 이름 |
navigator.userAgentData?.brands
// [{ brand: "Not A(Brand", version: "99" }, { brand: "Chromium", version: "…" }, { brand: "Google Chrome", version: "…" }]
navigator.userAgentData?.mobile // false- 기본으로는 이 세 개만 온다. 상세 버전·모델 같은 고엔트로피 값은 서버가
Accept-CH로 요청하거나 JS에서getHighEntropyValues()로 따로 받아야 한다. 필요한 것만 요청한다. - 헤더 값은 문자열이 아니라 구조화 헤더(Structured Headers) 다. 브라우저는 일부러 이상한 가짜 브랜드(
Not A(Brand같은)를 끼워 넣어 엉성한 파싱을 깨뜨린다. 명세에 맞는 파서로 읽고, 목록 순서·특정 문자열을 가정하지 않는다.
사이트가 할 일
서드파티 점검
- 서드파티 스크립트가
navigator.userAgent를 읽거나 보이지 않는<canvas>를 만드는지 본다(서드파티와 프라이버시). - 공급사 처리방침에서 확률적 매칭(probabilistic matching) 같은 표현을 찾는다. 로그인 ID로 잇는 결정적 매칭(deterministic matching)과 달리, 지문으로 추정해 잇는다는 뜻이다.
- 내가 필요한 만큼만 모은다(데이터 최소화).
넓게 시험하기
API를 막거나 값을 흐리는 정책은 브라우저마다 다르고, 같은 엔진이라도 API 동작이 다를 수 있다.
| 범위 | 시험 대상 |
|---|---|
| 엔진 | Blink(크롬·엣지·삼성 인터넷), Gecko(파이어폭스), WebKit(사파리), 데스크톱과 모바일 모두 |
| 기기 | 태블릿, 오래된 안드로이드, 업데이트가 늦은 기기(TV·게임기) |
| 빌드 | Safari Technology Preview, Chrome Canary, Firefox Beta |
| 상태 | 새 프로필, 시크릿·비공개 창, 권한 거부, 파이어폭스 지문 보호 모드 |
권한을 거부당했을 때 화면이 어떻게 안내하는지는 사람이 본다. 한 번 확인한 뒤의 API 회귀 시험은 Puppeteer의 launch({ args })나 Cypress의 브라우저 실행 옵션으로 브라우저 플래그를 바꿔 자동화한다.
정리
- 지문은 쿠키와 달리 보이지 않고 지울 수 없어서, 동의 없는 추적에 쓰인다.
- 엔트로피가 높은 특성일수록 사람을 잘 가르고, 브라우저는 그 값을 막거나 흐리거나 권한 뒤로 숨긴다.
- UA는 축소됐다. 브라우저 판별은 기능 감지로, 꼭 필요한 정보는 Client Hints로 요청한다.
- 권한 거부·비공개 모드·여러 엔진에서 시험해 "값이 없거나 부정확할 때"도 동작하게 만든다.
관련 노트: 서드파티 쿠키와 브라우저 추적 방지, 서드파티와 프라이버시, 데이터 최소화와 프라이버시 실천, 웹 보안: CSRF와 XSS