FPScanner

repository·master·Indexed 18 days ago

https://github.com/antoinevastel/fpscanner

A lightweight, self-hosted browser fingerprinting and bot detection library (v1.0.7) featuring encryption, obfuscation, and cross-context validation. It identifies automation frameworks like Selenium, Puppeteer, and Playwright, and detects headless Chrome or virtualization by analyzing browser signals and the fsid (Fingerprint ID). Includes support for custom encryption keys and provides implementation examples for Node.js and Python backends.

Tokens
9.4K
Snippets
28
Records
40
Agent score
67%

What's inside fpscanner

  1. Understand the Fingerprint ID (fsid) format

    master

    The fsid is a JA4-inspired, locality-preserving identifier structured into semantic sections. This allows for human readability and partial matching (e.g., comparing only the GPU section).

    Format: FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>

    Section Breakdown

    #SectionDescription
    1VersionFingerprint Scanner version (e.g., FS1)
    2Detectionn-bit bitmask of all fastBotDetectionDetails booleans
    3Automation<5-bit>h<hash> (Automation booleans + hash)
    4Device<W>x<H>c<cpu>m<mem>b<5-bit>h<hash> (Screen, CPU, Memory, Device booleans + hash)
    5Browserf<10-bit>e<8-bit>p<4-bit>h<hash> (Features, Extensions, Plugins bitmasks + hash)
    6Graphics<1-bit>h<hash> (hasModifiedCanvas + hash)
    7Codecs<1-bit>h<hash> (hasMediaSource + hash)
    8Locale<lang><n>t<tz>_h<hash> (Language, timezone + hash)
    9Contexts<4-bit>h<hash> (Mismatch and webdriver flags + hash)
  2. Understand the limits and non-goals of FPScanner

    master

    FPScanner is a collection of building blocks, not a complete fraud detection system. Users should be aware of the following:

    • Attacker Adaptation: As an open-source library, attackers can inspect and adapt to the code. The goal is to make abuse operationally expensive, not to achieve perfect secrecy.
    • Obfuscation Limits: Obfuscation is a friction mechanism, not a guarantee. Motivated attackers may still reverse-engineer the code.
    • Client-side Spoofing: All client-side signals can be spoofed. Fingerprints should be treated as representations/signals, not definitive verdicts.
    • Not an End-to-End Solution: The library does not provide dashboards, rule engines, or managed mitigation. For a production-grade solution with observability, consider a dedicated platform like Castle.
  3. Understand what FPScanner detects

    master

    FPScanner identifies automation frameworks and bot-like environments by analyzing specific browser signals and cross-context consistency. It detects:

    Automation Frameworks

    • Webdriver: navigator.webdriver === true (Selenium, Puppeteer, Playwright)
    • Webdriver Writable: Property descriptor checks (Puppeteer, Playwright)
    • Selenium Properties: Presence of $cdc_ or $wdc_ in document
    • CDP: Chrome DevTools Protocol runtime markers
    • Playwright: Presence of __playwright or __pw_* markers

    Environment & Hardware Signals

    • Headless Chrome: Missing window.chrome object or default 800x600 resolution
    • Virtualization/Containers: Unrealistic CPU core counts or impossible device memory values

    Cross-Context Validation

    Bots often fail to maintain consistency between different execution contexts. FPScanner checks for:

    • Iframe/Worker Mismatches: Webdriver detected in an iframe or web worker but not in the main window.
    • Platform Mismatches: Differences in platform identity between the main window and iframes or workers.
    • WebGL Mismatches: Differences in the WebGL renderer between the main window and workers.
  4. Decrypt fingerprints on the server (Node.js and Python)

    master

    The library uses a simple XOR cipher with Base64 encoding to protect the fingerprint payload. You must implement this on your server to read the data.

    Node.js Implementation

    function decryptFingerprint(ciphertext, key) {
      const encrypted = Buffer.from(ciphertext, 'base64');
      const keyBytes = Buffer.from(key, 'utf8');
      const decrypted = Buffer.alloc(encrypted.length);
    
      for (let i = 0; i < encrypted.length; i++) {
        decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
      }
    
      let fingerprint = JSON.parse(decrypted.toString('utf8'));
      // Handle double-JSON-encoding if present
      if (typeof fingerprint === 'string') {
        fingerprint = JSON.parse(fingerprint);
      }
      return fingerprint;
    }

    Python Implementation

    import base64
    import json
    
    def decrypt_fingerprint(ciphertext: str, key: str) -> dict:
        encrypted = base64.b64decode(ciphertext)
        key_bytes = key.encode('utf-8')
    
        decrypted = bytearray(len(encrypted))
        for i in range(len(encrypted)):
            decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]
    
        fingerprint = json.loads(decrypted.decode('utf-8'))
        # Handle double-JSON-encoding if present
        if isinstance(fingerprint, str):
            fingerprint = json.loads(fingerprint)
        return fingerprint
  5. Implement Server-Side Fingerprint Validation (Node.js)

    master

    On your server, you must decrypt the fingerprint and validate it to prevent replay attacks and detect bots.

    Note: The decryption logic uses a simple XOR cipher with the key provided during your build process (e.g., npx fpscanner build --key=your-key).

    To secure your endpoint:

    1. Decrypt the base64 ciphertext using your secret key.
    2. Check fastBotDetection: If true, the request is likely from an automated tool.
    3. Validate time: Compare the time field (Unix timestamp in ms) against the current time to ensure the fingerprint hasn't expired (e.g., older than 60 seconds) to prevent replay attacks.
    4. Use fsid: Use the fingerprint ID for session correlation and tracking.
    // Decrypt and validate the fingerprint
    // Use the same key you provided when building: npx fpscanner build --key=your-key
    const key = 'your-secret-key'; // Your custom key
    
    function decryptFingerprint(ciphertext, key) {
      const encrypted = Buffer.from(ciphertext, 'base64');
      const keyBytes = Buffer.from(key, 'utf8');
      const decrypted = Buffer.alloc(encrypted.length);
    
      for (let i = 0; i < encrypted.length; i++) {
        decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
      }
    
      return JSON.parse(decrypted.toString('utf8'));
    }
    
    app.post('/api/fingerprint', (req, res) => {
      const fingerprint = decryptFingerprint(req.body.fingerprint, key);
    
      // Check bot detection
      if (fingerprint.fastBotDetection) {
        console.log('🤖 Bot detected!', fingerprint.fastBotDetectionDetails);
        return res.status(403).json({ error: 'Bot detected' });
      }
    
      // Validate timestamp (prevent replay attacks)
      const ageMs = Date.now() - fingerprint.time;
      if (ageMs > 60000) { // 60 seconds
        return res.status(400).json({ error: 'Fingerprint expired' });
      }
    
      // Use fingerprint.fsid for session correlation
      console.log('Fingerprint ID:', fingerprint.fsid);
      res.json({ ok: true });
    });
  6. Use a Custom Encryption Key with FPScanner

    master

    To ensure security in production, you should use a custom encryption key instead of the default dev-key. The key used during the build process must match the key used by the server during decryption.

    1. Build the production library with your secret key:
      FINGERPRINT_KEY=your-secret-key npm run build:prod
    2. Run the server using the same secret key:
      FINGERPRINT_KEY=your-secret-key node demo-server.js
    FINGERPRINT_KEY=your-secret-key npm run build:prod
    FINGERPRINT_KEY=your-secret-key node demo-server.js
  7. Build FPScanner with a custom encryption key

    master

    For production, you should replace the placeholder key with your own and enable obfuscation to prevent attackers from forging payloads. Use the fpscanner build command.

    Key Injection Priority

    1. CLI Argument: --key=your-secret-key (Highest priority)
    2. Environment Variable: FINGERPRINT_KEY=your-secret-key
    3. .env file: FINGERPRINT_KEY=your-secret-key inside a .env file

    Usage Examples

    # Using a CLI argument
    npx fpscanner build --key=your-secret-key-here
    
    # Using a custom env file
    npx fpscanner build --env-file=.env.production
    
    # Skipping obfuscation (for development only)
    npx fpscanner build --key=dev-key --no-obfuscate

    CI/CD Integration

    You can automate the build in your CI/CD pipeline by adding a postinstall script to your package.json:

    {
      "scripts": {
        "postinstall": "fpscanner build"
      }
    }

    Then, set FINGERPRINT_KEY as a secret in your environment (e.g., GitHub Actions secrets).

    npx fpscanner build --key=your-secret-key-here
  8. Implement security best practices for FPScanner

    master

    To ensure the security of your fingerprinting implementation, follow these best practices:

    1. Key Management: Use a strong, random key (at least 32 characters) and rotate it regularly. Since the key is shipped client-side, rotation forces attackers to re-analyze the bundle.
    2. Obfuscation: Enable the library's built-in obfuscation for production builds to hide the encryption key and increase the cost of payload forgery.
    3. Server-side Validation:
      • Reject fingerprints older than a reasonable threshold (e.g., 60 seconds) to prevent replay attacks.
      • Optionally track and reject duplicate nonces.
    4. Monitoring: Monitor fingerprint distributions over time. Sudden spikes or unusual reuse patterns can indicate automated activity.
    5. Defense in Depth: Combine fingerprinting with other controls like rate limiting, behavioral analysis, and CAPTCHAs on sensitive endpoints (login, signup, etc.).
  9. Set up the FPScanner Python Demo

    master

    To run the Python demo, you must first build the fpscanner library with the development encryption key, then start the Python server. This demonstrates a full flow where the client collects an encrypted fingerprint and the Python server decrypts it.

    Prerequisites

    • Python 3.6+
    • fpscanner built with the dev-key (or a custom key)

    Steps

    1. Build the library (from the fpscanner root directory):
      npm run build:obfuscate
    2. Navigate to the example directory:
      cd examples/python
    3. Start the demo server:
      python3 demo-server.py
    4. Access the client: Open http://localhost:3000 in your browser.
    npm run build:obfuscate
    cd examples/python
    python3 demo-server.py
  10. Run the FPScanner Node.js Demo

    master

    This demo illustrates a full-stack fingerprinting flow: the client-side HTML page collects an encrypted fingerprint using the fpscanner library, and a Node.js server receives, decrypts, and logs the fingerprint data.

    Prerequisites

    • Node.js installed
    • fpscanner built with the dev-key (or your custom key)

    Setup Steps

    1. Build the library with the development encryption key from the fpscanner root directory:
      npm run build:obfuscate
    2. Navigate to the example directory:
      cd examples/nodejs
    3. Start the demo server:
      node demo-server.js
    4. Access the client by navigating to http://localhost:3000 in your browser.
    # From fpscanner root
    npm run build:obfuscate
    
    # From examples/nodejs
    node demo-server.js
  11. Resolve the FINGERPRINT_KEY encryption key

    master

    The CLI resolves the encryption key using the following priority order:

    1. CLI Argument: --key=KEY (Highest priority)
    2. Environment Variable: FINGERPRINT_KEY
    3. Environment File: FINGERPRINT_KEY inside a .env file (or a custom file specified via --env-file=FILE).

    If no key is found through these methods, the build will fail.

    # 1. Using CLI argument
    npx fpscanner build --key=my-secret-key
    
    # 2. Using environment variable
    export FINGERPRINT_KEY=my-secret-key
    npx fpscanner build
    
    # 3. Using .env file
    echo "FINGERPRINT_KEY=my-secret-key" >> .env
    npx fpscanner build
    
    # 3a. Using a custom env file
    npx fpscanner build --env-file=.env.production