Fingerprints Are the New Cookies: How Browser Fingerprinting Works and What Developers Should Do About It
Hồi trước, muốn track user thì cứ set một cái cookie là xong. Giờ thì khác rồi: Safari có ITP, Firefox có Total Cookie Protection, Chrome cũng siết third-party cookie, user thì bấm "Reject all" ngay khi thấy cookie banner. Nhưng tracking không biến mất, nó chỉ chuyển sang chỗ khác: browser fingerprinting. Không cần lưu gì trên máy user, chỉ cần gom đủ tín hiệu từ trình duyệt là có thể nhận ra một người với độ chính xác khá cao, kể cả khi họ dùng Incognito.
Bài này mình không viết để dạy track người khác. Mục tiêu là để dev Việt hiểu cơ chế, biết code của mình (hoặc SDK bên thứ ba mình nhúng vào) đang làm gì, và dùng fingerprinting đúng chỗ: chống fraud, chống bot, phát hiện account takeover. Không dùng để quảng cáo lén.
Fingerprint được tạo ra từ đâu?
Ý tưởng cốt lõi rất đơn giản: mỗi thuộc tính riêng lẻ thì nhiều người trùng nhau, nhưng ghép 20-30 thuộc tính lại thì tổ hợp gần như là duy nhất. Nghiên cứu Panopticlick của EFF từ năm 2010 đã cho thấy khoảng 83% trình duyệt có fingerprint unique, và đến giờ con số đó chỉ có tăng.
Các nhóm tín hiệu phổ biến:
- Passive (server thấy luôn): IP,
User-Agent,Accept-Language, thứ tự HTTP header, TLS ClientHello (JA3/JA4), HTTP/2 SETTINGS frame. -
- Active (cần chạy JS): Canvas rendering, WebGL renderer/vendor, AudioContext, danh sách font,
screen.width/height,devicePixelRatio,hardwareConcurrency, timezone,navigator.languages.
- Active (cần chạy JS): Canvas rendering, WebGL renderer/vendor, AudioContext, danh sách font,
flowchart LR
A[Browser] -->|TLS ClientHello| B[JA4 hash]
A -->|HTTP headers| C[Header fingerprint]
A -->|JS chạy trên trang| D[Canvas / WebGL / Audio]
A -->|JS| E[Screen, fonts, timezone]
B --> F[Combine + hash]
C --> F
D --> F
E --> F
F --> G[Visitor ID]
```
Điểm đáng sợ nhất là canvas fingerprint: cùng một đoạn vẽ text + gradient, nhưng GPU, driver, font rendering, anti-aliasing khác nhau sẽ ra pixel khác nhau. Hash cái output đó lại là có một ID khá ổn định.
## Tự viết một fingerprint đơn giản bằng JavaScript
Đoạn dưới đây chạy được trên Chrome 120+, Firefox 120+, Safari 17+. Mình dùng `crypto.subtle` có sẵn, không cần thư viện:
```javascript
async function sha256(text) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
}
function canvasSignal() {
const c = document.createElement('canvas');
c.width = 240; c.height = 60;
const ctx = c.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '16px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(100, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('Xin chào, fingerprint 🍜', 2, 15);
return c.toDataURL();
}
function webglSignal() {
const gl = document.createElement('canvas').getContext('webgl');
if (!gl) return 'no-webgl';
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : 'masked';
}
async function getFingerprint() {
const signals = {
ua: navigator.userAgent,
langs: navigator.languages.join(','),
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${screen.width}x${screen.height}@${devicePixelRatio}`,
cores: navigator.hardwareConcurrency,
canvas: await sha256(canvasSignal()),
webgl: webglSignal(),
};
return { id: await sha256(JSON.stringify(signals)), signals };
}
getFingerprint().then(fp => console.table(fp.signals));
```
Mở Chrome thường rồi mở Incognito, chạy lại: bạn sẽ thấy `id` y hệt nhau. Đó chính là lý do người ta nói fingerprint là cookie mới, chỉ khác là user không xóa được.
Thử trên Firefox với `privacy.resistFingerprinting = true` trong `about:config`, hoặc trên Brave/Tor Browser: canvas sẽ bị thêm noise hoặc trả về dữ liệu chuẩn hóa, timezone thành UTC, screen size bị làm tròn. ID sẽ đổi mỗi session hoặc trùng với hàng nghìn người khác.
## Phía server: passive fingerprint không cần JavaScript
Nhiều bot dùng headless browser nên JS fingerprint có thể bị giả. Server-side fingerprint khó giả hơn vì nó nằm ở tầng network. Ví dụ đơn giản với FastAPI 0.115, gom các header và thứ tự của chúng:
```python
import hashlib
from fastapi import FastAPI, Request
app = FastAPI()
SIGNAL_HEADERS = ['user-agent', 'accept', 'accept-language', 'accept-encoding',
'sec-ch-ua', 'sec-ch-ua-platform', 'sec-fetch-site']
@app.middleware('http')
async def passive_fp(request: Request, call_next):
# Thứ tự header raw là tín hiệu rất mạnh: curl, requests, Chrome đều khác nhau
order = ','.join(k.decode().lower() for k, _ in request.scope['headers'])
values = '|'.join(request.headers.get(h, '-') for h in SIGNAL_HEADERS)
ja4 = request.headers.get('x-ja4', '-') # do reverse proxy gắn vào
raw = f'{order}#{values}#{ja4}'
request.state.fp = hashlib.sha256(raw.encode()).hexdigest()[:16]
# Chrome thật luôn gửi sec-ch-ua; UA nói là Chrome mà thiếu thì đáng nghi
ua = request.headers.get('user-agent', '')
request.state.suspicious = 'Chrome/' in ua and 'sec-ch-ua' not in request.headers
return await call_next(request)
@app.get('/whoami')
async def whoami(request: Request):
return {'fp': request.state.fp, 'suspicious': request.state.suspicious}
```
Test nhanh bằng curl để thấy sự khác biệt:
```bash
# curl giả UA Chrome nhưng thiếu client hints -> suspicious = true
curl -s -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/129.0 Safari/537.36' \
http://localhost:8000/whoami
# Xem TLS fingerprint của chính bạn (cần curl 8.x)
curl -sv https://localhost:8443 2>&1 | grep -E 'TLS|ALPN'
```
JA4 (bản kế nhiA3, do FoxIO công bố năm 2023) hash các thông tin trong TLS ClientHello như cipher suites, extensions, ALPN. Python `requests` có JA4 khác hẳn Chrome, nên dù bot có giả User-Agent hoàn hảo thì tầng TLS vẫn lộ. Nginx có module, HAProxy 3.x và nhiều CDN đã hỗ trợ tính JA4 rồi gắn vào header cho backend.
```mermaid
sequenceDiagram
participant C as Client
participant P as Reverse Proxy
participant A as App
participant R as Risk Engine
C->>P: TLS ClientHello
P->>P: Tính JA4
C->>P: HTTP request + headers
P->>A: Forward + x-ja4
A->>R: fp + JS fingerprint + IP
R-->>A: score (allow / challenge / block)
A-->>C: Response hoặc CAPTCHA
```
## Dùng đúng chỗ: fraud, không phải tracking
Đây là phần dev hay bỏ qua. Ở Việt Nam, **Nghị định 13/2023/NĐ-CP** về bảo vệ dữ liệu cá nhân đã có hiệu lực, và Luật Bảo vệ dữ liệu cá nhân 2025 tiếp tục siết chặt hơn. Ở EU thì GDPR và ePrivacy Directive coi fingerprinting là xử lý dữ liệu cá nhân, cần consent giống hệt cookie. Nếu sản phẩm của bạn có user châu Âu thì đừng nghĩ fingerprint là cách lách cookie banner.
Vài nguyên tắc mình áp dụng:
1. **Purpose limitation**: chỉ dùng fingerprint ở các flow nhạy cảm như login, checkout, đổi mật khẩu, tạo tài khoản hàng loạt. Không gửi sang analytics hay ad network.
2. **Fingerprint là tín hiệu, không phải danh tính**: dùng như một input cho risk score. Fingerprint mới + IP lạ + đăng nhập lúc 3h sáng thì mới bật 2FA challenge, đừng block cứng chỉ vì fingerprint đổi.
3. **Retention ngắn**: lưu hash, đặt TTL 30-90 ngày, không lưu raw canvas data.
4. **Audit SDK bên thứ ba**: mở DevTools, tab Network/Sources, search `toDataURL`, `getImageData`, `UNMASKED_RENDERER`, `OfflineAudioContext`. Nhiều SDK chat, analytics, A/B testing đang fingerprint user mà team không biết.
5. **Đừng phạt user quan tâm privacy**: người dùng Brave, Firefox RFP, Safari sẽ có fingerprint thay đổi liên tục. Nếu hệ thống coi đó là bot thì bạn đang chặn nhầm chính những user cẩn thận nhất.
## Kết luận
Cookie đang chết dần, fingerprinting đang lên, và nó vô hình với user hơn nhiều. Là developer, bạn nên:
- **Hiểu cơ chế**: chạy đoạn JS ở trên trên vài trình duyệt khác nhau, so sánh kết quả để thấy tận mắt độ ổn định của từng tín hiệu.
- **Kết hợp nhiều tầng**: JS fingerprint dễ giả, header và JA4 ở tầng network khó giả hơn. Dùng cả hai cho bài toán chống bot.
- **Audit code và SDK**: grep codebase và bundle production tìm các API canvas, WebGL, audio. Biết chính xác ai đang thu thập gì.
- **Tuân thủ pháp lý**: coi fingerprint là dữ liệu cá nhân, ghi rõ trong privacy policy, xin consent khi cần, đặt TTL.
- **Test với trình duyệt privacy-focused**: đảm bảo Firefox RFP, Brave, Safari không bị hệ thống chống fraud của bạn chặn nhầm.
Fingerprintin
All Rights Reserved