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
JavaScript
'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