UNPKG

fingerprinter-js

Version:

A modern JavaScript library for generating unique and reliable browser fingerprints with built-in bot detection

842 lines (833 loc) 29.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * Suspect Score Analyzer * Analyzes suspicious signals in browser fingerprints */ class SuspectAnalyzer { /** * Analyzes a fingerprint to detect suspicious signals */ static analyze(components) { const signals = []; let totalScore = 0; // 1. Automation detection const automationSignals = this.checkAutomation(components); signals.push(...automationSignals); // 2. Technical inconsistencies const consistencySignals = this.checkConsistency(components); signals.push(...consistencySignals); // 3. Suspicious environment const environmentSignals = this.checkEnvironment(components); signals.push(...environmentSignals); // 4. Bot patterns const botSignals = this.checkBotPatterns(components); signals.push(...botSignals); // Calcul du score total const detectedSignals = signals.filter((s) => s.detected); totalScore = detectedSignals.reduce((sum, signal) => sum + signal.severity * 10, 0); totalScore = Math.min(100, totalScore); // Cap à 100 const riskLevel = this.calculateRiskLevel(totalScore); return { score: totalScore, signals: detectedSignals, riskLevel, details: { totalSignalsDetected: detectedSignals.length, highSeveritySignals: detectedSignals.filter((s) => s.severity >= 8) .length, automationDetected: automationSignals.some((s) => s.detected), inconsistenciesFound: consistencySignals.some((s) => s.detected), }, }; } /** * Detects automation signals */ static checkAutomation(components) { const signals = []; // WebDriver detection signals.push({ type: "webdriver", severity: 9, description: "WebDriver automation detected", detected: this.hasWebDriver(), }); // Headless browser signals.push({ type: "headless", severity: 8, description: "Headless browser detected", detected: this.isHeadless(components), }); // Phantom/Automation signatures signals.push({ type: "phantom", severity: 7, description: "PhantomJS signatures detected", detected: this.hasPhantomSignatures(components), }); // Selenium signatures signals.push({ type: "selenium", severity: 8, description: "Selenium signatures detected", detected: this.hasSeleniumSignatures(), }); return signals; } /** * Checks data consistency */ static checkConsistency(components) { const signals = []; // Timezone vs Language inconsistency signals.push({ type: "timezone_language", severity: 5, description: "Timezone/language inconsistency", detected: this.hasTimezoneLanguageInconsistency(components), }); // Screen resolution inconsistency signals.push({ type: "screen_consistency", severity: 6, description: "Screen resolution inconsistency", detected: this.hasScreenInconsistency(components), }); // Canvas fingerprint too generic signals.push({ type: "generic_canvas", severity: 4, description: "Canvas fingerprint too generic", detected: this.hasGenericCanvas(components), }); return signals; } /** * Checks execution environment */ static checkEnvironment(components) { const signals = []; // Missing expected APIs signals.push({ type: "missing_apis", severity: 6, description: "Missing browser APIs", detected: this.hasMissingAPIs(components), }); // Too many errors in collection signals.push({ type: "collection_errors", severity: 5, description: "Too many collection errors", detected: this.hasTooManyErrors(components), }); // Suspicious user agent signals.push({ type: "suspicious_ua", severity: 7, description: "Suspicious user agent", detected: this.hasSuspiciousUserAgent(components), }); return signals; } /** * Detects bot patterns */ static checkBotPatterns(components) { const signals = []; // Perfect fingerprint (too stable) signals.push({ type: "too_perfect", severity: 3, description: "Fingerprint too perfect/stable", detected: this.isTooStable(components), }); // Known bot signatures signals.push({ type: "bot_signature", severity: 9, description: "Known bot signature detected", detected: this.hasKnownBotSignature(components), }); return signals; } // Specific detection methods static hasWebDriver() { return (typeof window !== "undefined" && window.navigator.webdriver === true); } static isHeadless(components) { const ua = components.userAgent || ""; return (ua.includes("HeadlessChrome") || ua.includes("PhantomJS") || (typeof window !== "undefined" && window.outerHeight === 0)); } static hasPhantomSignatures(components) { const ua = components.userAgent || ""; return (ua.includes("PhantomJS") || (typeof window !== "undefined" && window._phantom)); } static hasSeleniumSignatures() { if (typeof window === "undefined") return false; return !!(window.selenium || window.webdriver || window.document.selenium || window.document.webdriver || window.navigator.webdriver); } static hasTimezoneLanguageInconsistency(components) { const timezone = components.timezone; const language = components.language; if (!timezone || !language) return false; // Simplified logic - in a real system, use a database // of timezone/country/language mappings const suspiciousCombinations = [ { tz: "America/New_York", lang: "zh-CN" }, { tz: "Europe/Paris", lang: "ja-JP" }, { tz: "Asia/Tokyo", lang: "es-ES" }, ]; return suspiciousCombinations.some((combo) => timezone.includes(combo.tz) && Array.isArray(language) && language.some((l) => l.includes(combo.lang))); } static hasScreenInconsistency(components) { const screen = components.screen; if (!screen) return false; // Résolutions trop parfaites ou communes aux bots const suspiciousResolutions = [ "1024x768", "800x600", "1280x720", "1920x1080", ]; const resolution = `${screen.width}x${screen.height}`; return (suspiciousResolutions.includes(resolution) && screen.colorDepth === 24); // Combinaison suspecte } static hasGenericCanvas(components) { const canvas = components.canvas; if (!canvas || typeof canvas !== "string") return false; // Simplified - in a real system, hash the canvas return canvas.length < 100 || canvas === "no-canvas"; } static hasMissingAPIs(components) { const expectedAPIs = ["userAgent", "language", "screen"]; const missingAPIs = expectedAPIs.filter((api) => !components[api] || components[api].error); return missingAPIs.length > 1; } static hasTooManyErrors(components) { const errorCount = Object.values(components).filter((value) => value && typeof value === "object" && value.error).length; return errorCount > 3; } static hasSuspiciousUserAgent(components) { const ua = components.userAgent || ""; const suspiciousPatterns = [ "HeadlessChrome", "PhantomJS", "bot", "crawler", "spider", "scraper", ]; return suspiciousPatterns.some((pattern) => ua.toLowerCase().includes(pattern.toLowerCase())); } static isTooStable(components) { // Si tous les composants sont parfaits, c'est suspect const perfectComponents = Object.values(components).filter((value) => value && !value.error && value !== "unknown").length; return perfectComponents === Object.keys(components).length; } static hasKnownBotSignature(components) { const ua = components.userAgent || ""; // Database of known bot signatures const botSignatures = [ "Googlebot", "Bingbot", "facebookexternalhit", "Twitterbot", "LinkedInBot", "WhatsApp", "python-requests", "curl/", "wget", ]; return botSignatures.some((signature) => ua.includes(signature)); } static calculateRiskLevel(score) { if (score < 30) return "LOW"; if (score < 70) return "MEDIUM"; return "HIGH"; } } /** * Utility functions for fingerprinting */ /** * Simple hash function to convert string to number */ function simpleHash(str) { let hash = 0; if (str.length === 0) return hash; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash); } /** * Generate SHA-256 hash (using Web Crypto API if available) */ async function sha256(message) { if (typeof crypto !== "undefined" && crypto.subtle) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); return hashHex; } // Fallback to simple hash if crypto API is not available return simpleHash(message).toString(16); } /** * Check if running in browser environment */ function isBrowser() { return typeof window !== "undefined" && typeof document !== "undefined"; } /** * Safe JSON stringify that handles circular references */ function safeStringify(obj) { const seen = new WeakSet(); return JSON.stringify(obj, (key, val) => { if (val != null && typeof val === "object") { if (seen.has(val)) { return "[Circular]"; } seen.add(val); } return val; }); } /** * Get a value safely from an object with error handling */ function safeGet(fn, defaultValue) { try { return fn(); } catch (_a) { return defaultValue; } } class AudioCollector { constructor() { this.name = "audio"; } async collect() { return safeGet(async () => { // Check if AudioContext is available const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) { return { error: "no-audio-context" }; } const audioContext = new AudioContext(); const result = {}; try { // Get basic audio context info result.sampleRate = audioContext.sampleRate; result.state = audioContext.state; result.maxChannelCount = audioContext.destination.maxChannelCount; result.channelCount = audioContext.destination.channelCount; result.channelCountMode = audioContext.destination.channelCountMode; result.channelInterpretation = audioContext.destination.channelInterpretation; // Create audio fingerprint using oscillator const oscillator = audioContext.createOscillator(); const analyser = audioContext.createAnalyser(); const gainNode = audioContext.createGain(); const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1); gainNode.gain.setValueAtTime(0, audioContext.currentTime); oscillator.type = "triangle"; oscillator.frequency.setValueAtTime(10000, audioContext.currentTime); oscillator.connect(analyser); analyser.connect(scriptProcessor); scriptProcessor.connect(gainNode); gainNode.connect(audioContext.destination); scriptProcessor.onaudioprocess = () => { const freqData = new Float32Array(analyser.frequencyBinCount); analyser.getFloatFrequencyData(freqData); let sum = 0; for (let i = 0; i < freqData.length; i++) { sum += Math.abs(freqData[i]); } result.audioFingerprint = sum.toString(); }; oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); // Wait a bit for processing await new Promise((resolve) => setTimeout(resolve, 100)); // Clean up await audioContext.close(); return result; } catch (error) { try { await audioContext.close(); } catch (_a) { } return { error: "audio-processing-failed", details: error.message, }; } }, Promise.resolve({ error: "audio-not-available" })); } } class FontsCollector { constructor() { this.name = "fonts"; } collect() { return safeGet(() => { const testFonts = [ "Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT Bold", "Bookman Old Style", "Bradley Hand ITC", "Century", "Century Gothic", "Comic Sans MS", "Courier", "Courier New", "Georgia", "Gentium", "Helvetica", "Helvetica Neue", "Impact", "King", "Lucida Console", "Lalit", "Modena", "Monotype Corsiva", "Papyrus", "Tahoma", "TeX", "Times", "Times New Roman", "Trebuchet MS", "Verdana", "Verona", ]; const availableFonts = []; const testString = "mmmmmmmmmmlli"; const testSize = "72px"; const h = document.getElementsByTagName("body")[0]; // Create a test span element const s = document.createElement("span"); s.style.fontSize = testSize; s.style.position = "absolute"; s.style.left = "-9999px"; s.style.visibility = "hidden"; s.innerHTML = testString; h.appendChild(s); // Get default width and height s.style.fontFamily = "monospace"; const defaultWidth = s.offsetWidth; const defaultHeight = s.offsetHeight; // Test each font for (const font of testFonts) { s.style.fontFamily = `'${font}', monospace`; if (s.offsetWidth !== defaultWidth || s.offsetHeight !== defaultHeight) { availableFonts.push(font); } } // Clean up h.removeChild(s); return availableFonts; }, []); } } class UserAgentCollector { constructor() { this.name = "userAgent"; } collect() { return safeGet(() => navigator.userAgent, "unknown"); } } class LanguageCollector { constructor() { this.name = "language"; } collect() { return safeGet(() => { const languages = []; if (navigator.language) { languages.push(navigator.language); } if (navigator.languages) { languages.push(...navigator.languages); } return [...new Set(languages)]; // Remove duplicates }, ["unknown"]); } } class TimezoneCollector { constructor() { this.name = "timezone"; } collect() { return safeGet(() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }, "unknown"); } } class ScreenCollector { constructor() { this.name = "screen"; } collect() { return safeGet(() => ({ width: screen.width, height: screen.height, availWidth: screen.availWidth, availHeight: screen.availHeight, colorDepth: screen.colorDepth, pixelDepth: screen.pixelDepth, devicePixelRatio: window.devicePixelRatio || 1, }), { width: 0, height: 0, availWidth: 0, availHeight: 0, colorDepth: 0, pixelDepth: 0, devicePixelRatio: 1, }); } } class PluginsCollector { constructor() { this.name = "plugins"; } collect() { return safeGet(() => { const plugins = []; for (let i = 0; i < navigator.plugins.length; i++) { const plugin = navigator.plugins[i]; plugins.push({ name: plugin.name, description: plugin.description, filename: plugin.filename, }); } return plugins; }, []); } } class CanvasCollector { constructor() { this.name = "canvas"; } collect() { return safeGet(() => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return "no-canvas-context"; // Set canvas size canvas.width = 200; canvas.height = 50; // Draw text with various styles ctx.textBaseline = "top"; ctx.font = "14px Arial"; ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText("Canvas fingerprint 🎨", 2, 15); ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText("Canvas fingerprint 🎨", 4, 17); // Draw some shapes ctx.globalCompositeOperation = "multiply"; ctx.fillStyle = "rgb(255,0,255)"; ctx.beginPath(); ctx.arc(50, 50, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "rgb(0,255,255)"; ctx.beginPath(); ctx.arc(100, 50, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "rgb(255,255,0)"; ctx.beginPath(); ctx.arc(75, 100, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); return canvas.toDataURL(); }, "no-canvas"); } } class WebGLCollector { constructor() { this.name = "webgl"; } collect() { return safeGet(() => { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) return { error: "no-webgl-context" }; const result = {}; // Get basic WebGL info result.vendor = gl.getParameter(gl.VENDOR); result.renderer = gl.getParameter(gl.RENDERER); result.version = gl.getParameter(gl.VERSION); result.shadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION); // Get supported extensions const extensions = gl.getSupportedExtensions(); result.extensions = extensions ? extensions.sort() : []; // Get additional parameters result.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); result.maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS); result.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); result.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); result.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); result.maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS); // Get unmasked info if available const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (debugInfo) { result.unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL); result.unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); } return result; }, { error: "webgl-not-available" }); } } /** * Main Fingerprint class */ class Fingerprint { constructor(options = {}) { this.collectors = []; this.options = options; this.initializeCollectors(); } initializeCollectors() { // Always include basic collectors this.collectors.push(new UserAgentCollector()); if (!this.options.excludeLanguage) { this.collectors.push(new LanguageCollector()); } if (!this.options.excludeTimezone) { this.collectors.push(new TimezoneCollector()); } if (!this.options.excludeScreenResolution) { this.collectors.push(new ScreenCollector()); } if (!this.options.excludePlugins) { this.collectors.push(new PluginsCollector()); } if (!this.options.excludeCanvas) { this.collectors.push(new CanvasCollector()); } if (!this.options.excludeWebGL) { this.collectors.push(new WebGLCollector()); } if (!this.options.excludeAudio) { this.collectors.push(new AudioCollector()); } if (!this.options.excludeFonts) { this.collectors.push(new FontsCollector()); } } /** * Normalize custom data to ensure stability by removing temporal values */ normalizeCustomData(data) { if (!data || typeof data !== "object") { return data; } const normalized = { ...data }; // List of keys that are known to cause instability const unstableKeys = [ "timestamp", "time", "now", "date", "random", "rand", "nonce", "salt", "sessionId", "requestId", "uuid", "performance", "timing", ]; // Remove unstable keys for (const key of unstableKeys) { if (key in normalized) { delete normalized[key]; } } // Also check for values that look like timestamps or random data for (const [key, value] of Object.entries(normalized)) { if (typeof value === "number") { // Remove values that look like timestamps (very large numbers) if (value > 1000000000000 && value < 9999999999999) { // Likely timestamp delete normalized[key]; } // Remove values that change too rapidly (potential random numbers) if (Math.random && value === Math.random()) { delete normalized[key]; } } if (typeof value === "string") { // Remove UUIDs or similar patterns if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) { delete normalized[key]; } } } return normalized; } /** * Generate a fingerprint */ async generate() { if (!isBrowser()) { throw new Error("Fingerprinting is only available in browser environments"); } const components = {}; let confidence = 0; // Collect all components for (const collector of this.collectors) { try { const data = await collector.collect(); components[collector.name] = data; // Calculate confidence based on available components if (data && data !== "unknown" && data !== "no-canvas" && !data.error) { confidence += 1; } } catch (error) { components[collector.name] = { error: error.message }; } } // Add custom data if provided if (this.options.customData) { let customDataToUse = this.options.customData; // Normalize custom data unless explicitly disabled if (!this.options.allowUnstableData) { customDataToUse = this.normalizeCustomData(this.options.customData); } if (Object.keys(customDataToUse).length > 0) { components.custom = customDataToUse; confidence += 0.5; } } // Calculate confidence percentage const maxConfidence = this.collectors.length + (this.options.customData ? 0.5 : 0); const confidencePercentage = Math.round((confidence / maxConfidence) * 100); // Generate fingerprint hash const dataString = safeStringify(components); const fingerprint = await sha256(dataString); // Generate suspect analysis if requested let suspectAnalysis = undefined; if (this.options.includeSuspectAnalysis) { suspectAnalysis = SuspectAnalyzer.analyze(components); } return { fingerprint, components, confidence: confidencePercentage, suspectAnalysis, }; } /** * Get component data without generating hash */ async getComponents() { if (!isBrowser()) { throw new Error("Fingerprinting is only available in browser environments"); } const components = {}; for (const collector of this.collectors) { try { const data = await collector.collect(); components[collector.name] = data; } catch (error) { components[collector.name] = { error: error.message }; } } if (this.options.customData) { let customDataToUse = this.options.customData; // Normalize custom data unless explicitly disabled if (!this.options.allowUnstableData) { customDataToUse = this.normalizeCustomData(this.options.customData); } if (Object.keys(customDataToUse).length > 0) { components.custom = customDataToUse; } } return components; } /** * Static method to quickly generate a fingerprint with default options */ static async generate(options = {}) { const fp = new Fingerprint(options); return fp.generate(); } /** * Static method to get available collectors */ static getAvailableCollectors() { return [ "userAgent", "language", "timezone", "screen", "plugins", "canvas", "webgl", "audio", "fonts", ]; } } exports.AudioCollector = AudioCollector; exports.CanvasCollector = CanvasCollector; exports.Fingerprint = Fingerprint; exports.FontsCollector = FontsCollector; exports.LanguageCollector = LanguageCollector; exports.PluginsCollector = PluginsCollector; exports.ScreenCollector = ScreenCollector; exports.TimezoneCollector = TimezoneCollector; exports.UserAgentCollector = UserAgentCollector; exports.WebGLCollector = WebGLCollector; exports.default = Fingerprint; exports.isBrowser = isBrowser; exports.safeGet = safeGet; exports.safeStringify = safeStringify; exports.sha256 = sha256; exports.simpleHash = simpleHash; //# sourceMappingURL=index.js.map