Parent : [[INDEX-CYBERSECU]]
Tracking de masse navigateur - scooby.csv, Accept-Language fingerprinting, timezone tracking
Problème
Un fichier CSV de 259 Mo sans en-tête, 4 173 645 lignes, couvrant 12 mois de collecte (septembre 2024 à septembre 2025). Chaque ligne enregistre un événement de navigation : user_id, timezone, langues_navigateur, timestamp. 543 448 utilisateurs distincts sont trackés. Le fuseau horaire révèle la localisation géographique : 64,9% sous Europe/Paris, puis Bruxelles, Toronto, Zurich, Casablanca. Les langues du navigateur ne sont pas un simple "fr" : c'est la liste complète Accept-Language, par exemple fr-FR,es-CO,es,en-GB,fr,en-US,en,fr, une combinaison quasi unique par utilisateur.
Contexte archive Ygg
- Source :
https://yggleak.top/fr/home/ygg-dossier - Fichier :
scooby.csv - Taille : 259 Mo, 4 173 645 lignes
- Période : Septembre 2024 à Septembre 2025 (12 mois)
- Utilisateurs distincts : 543 448
- Données : user_id, timezone, langues_navigateur (Accept-Language), timestamp
- Même user_id apparaît plusieurs fois par session, à quelques secondes d'intervalle
Technique de tracking
1. Accept-Language fingerprinting
// Le serveur récupère le header Accept-Language complet
const acceptLanguage = navigator.languages.join(',');
// Exemple : "fr-FR,es-CO,es,en-GB,fr,en-US,en,fr"
// Cette combinaison est quasi unique par utilisateur
Pourquoi c'est puissant :
- La plupart des utilisateurs ont 2-4 langues préférées
- L'ordre et la combinaison sont quasi uniques (entropie élevée)
- C'est une empreinte digitale stable (ne change pas souvent)
- Corrélation avec le timezone = profiling géographique précis
2. Timezone tracking
// Le serveur récupère le timezone
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Exemple : "Europe/Paris", "America/Toronto", "Africa/Casablanca"
Résultats YGG :
- 64,9% Europe/Paris
- Bruxelles
- Toronto
- Zurich
- Casablanca
3. Journal de navigation complet
Le même user_id apparaît plusieurs fois par session, à quelques secondes d'intervalle. Ce n'est pas un compteur de visites : c'est un journal de navigation qui révèle :
- Les habitudes de connexion
- Les horaires de connexion
- Le rythme de lecture
- La fréquence des visites
Solution protectif — Protection contre le tracking de masse
1. Détection du tracking côté serveur
# detect_mass_tracking.py — Détecter si un site fait du tracking excessif
def detect_mass_tracking(request_logs):
"""
request_logs : liste de dicts avec user_id, timestamp, path, ip, headers
"""
from collections import defaultdict
import statistics
user_requests = defaultdict(list)
for log in request_logs:
user_requests[log['user_id']].append(log)
suspicious_users = []
for user_id, requests in user_requests.items():
# 1. Trop de requêtes par session
if len(requests) > 100:
# 2. Intervalle entre requêtes très court (< 5 secondes)
timestamps = sorted([r['timestamp'] for r in requests])
intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
if intervals:
avg_interval = statistics.mean(intervals)
if avg_interval < 5: # Moins de 5 secondes entre requêtes
suspicious_users.append({
'user_id': user_id,
'request_count': len(requests),
'avg_interval': avg_interval,
'reason': 'excessive_tracking'
})
# 3. Collecte de données fingerprinting
if requests:
first_request = requests[0]
headers = first_request.get('headers', {})
fingerprinting_headers = ['Accept-Language', 'Accept-Encoding', 'DNT']
collected = [h for h in fingerprinting_headers if h in headers]
if len(collected) >= 2 and len(requests) > 50:
suspicious_users.append({
'user_id': user_id,
'collected_headers': collected,
'request_count': len(requests),
'reason': 'fingerprinting_collection'
})
return suspicious_users
2. Extension navigateur anti-tracking
// anti-tracking-extension.js — Bloquer le tracking de masse
chrome.webRequest.onBeforeSendHeaders.addListener(
function(details) {
// Bloquer ou modifier les headers fingerprinting
const headers = details.requestHeaders;
// 1. Modifier Accept-Language pour une valeur standard
const langHeader = headers.find(h => h.name.toLowerCase() === 'accept-language');
if (langHeader) {
// Utiliser une valeur commune (pas la liste complète)
langHeader.value = 'en-US,en;q=0.9';
}
// 2. Supprimer DNT (Do Not Track) — inutile et fingerprintable
const dntHeader = headers.find(h => h.name.toLowerCase() === 'dnt');
if (dntHeader) {
const index = headers.indexOf(dntHeader);
headers.splice(index, 1);
}
// 3. Supprimer les headers custom fingerprinting
const suspiciousHeaders = [
'x-timezone',
'x-browser-language',
'x-fingerprint',
'x-device-id'
];
headers.forEach((header, index) => {
if (suspiciousHeaders.includes(header.name.toLowerCase())) {
headers.splice(index, 1);
}
});
return { requestHeaders: headers };
},
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"]
);
3. Timezone spoofing
// timezone-spoof.js — Spoofer le timezone
// Écraser Intl.DateTimeFormat pour retourner un timezone générique
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
Intl.DateTimeFormat.prototype.resolvedOptions = function(...args) {
const result = originalResolvedOptions.call(this, ...args);
result.timeZone = 'UTC'; // Retourner UTC pour tout le monde
return result;
};
// Écraser Date.prototype.getTimezoneOffset
Date.prototype.getTimezoneOffset = function() {
return 0; // UTC = offset 0
};
4. Accept-Language standardisation
// standardize-language.js — Standardiser le Accept-Language
Object.defineProperty(navigator, 'languages', {
get() {
return ['en-US', 'en'];
},
configurable: true
});
Object.defineProperty(navigator, 'language', {
get() {
return 'en-US';
},
configurable: true
});
5. Détection de tracking côté serveur (pour les développeurs)
# tracking_audit.py — Auditer les pratiques de tracking
class TrackingAuditor:
def __init__(self, site_url):
self.site_url = site_url
def audit_tracking(self):
import requests
from bs4 import BeautifulSoup
response = requests.get(self.site_url)
soup = BeautifulSoup(response.text, 'html.parser')
alerts = []
# 1. Rechercher les scripts de tracking
scripts = soup.find_all('script')
for script in scripts:
if script.get('src'):
src = script['src']
if any(tracker in src for tracker in ['google-analytics', 'facebook', 'scooby', 'tracking']):
alerts.append({
'type': 'external_tracker',
'url': src
})
elif script.string:
if 'navigator.languages' in script.string:
alerts.append({
'type': 'language_fingerprinting',
'script': script.string[:200]
})
if 'Intl.DateTimeFormat' in script.string:
alerts.append({
'type': 'timezone_fingerprinting',
'script': script.string[:200]
})
# 2. Rechercher les endpoints de collecte
forms = soup.find_all('form')
for form in forms:
action = form.get('action', '')
if 'track' in action or 'collect' in action or 'log' in action:
alerts.append({
'type': 'tracking_endpoint',
'action': action
})
return alerts
Checklist de validation
- Pas de collecte de l'Accept-Language complet (trop fingerprintable)
- Pas de collecte du timezone (géolocalisation)
- Pas de journal de navigation à granularité seconde
- Pas de tracking cross-session sans consentement
- Headers Accept-Language standardisés (extension)
- Timezone spoofé (UTC) pour les sites sensibles
- Pas de headers custom fingerprinting (x-fingerprint, x-device-id)
- Consentement explicite pour le tracking analytique
- Anonymisation des logs (pas de user_id en clair)
- Pas de corrélation Accept-Language + timezone + IP
- DNT respecté (mais pas fiable)
- Audit régulier des scripts de tracking
Références
- Archive Ygg :
scooby.csv(YGGLeak) - EFF : Browser Fingerprinting
- OWASP : Privacy Risks