Explorer
KNOW-PAT-076

Tracking de masse navigateur - scooby.csv, Accept-Language fingerprinting, timezone tracking

Domaine
forensics
Type
pattern
Priorité
P2

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