Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Fingerprint Record Filtering #349

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

0xARYA
Copy link

@0xARYA 0xARYA commented Jan 23, 2025

An attempt to close #342

@0xARYA 0xARYA changed the title Feat/fingerprint filtering feat: Fingerprint Record Filtering Jan 23, 2025
@0xARYA
Copy link
Author

0xARYA commented Jan 24, 2025

The only additional validation step we can take with the current data model is ensuring that the client-side hints align with the ones provided in the headers.

Another potential step would be validating the plugins and mime-types array against known values.

@0xARYA
Copy link
Author

0xARYA commented Jan 24, 2025

I’m just reviewing my changes, but I’m currently away from my main computer. From a second glance, the aspect ratio checks should probably be removed—they now feel unnecessarily strict to me.

@0xARYA
Copy link
Author

0xARYA commented Jan 24, 2025

Please disregard for now, spotted major flaws that need to be corrected... I setup a parallel environment to test it on current fingerprints as I don't have access to the raw records.

@0xARYA
Copy link
Author

0xARYA commented Jan 25, 2025

For anyone tracking this PR, I’ve got a function you can use internally (I’m already using it and have validated it to work).

Here’s the function:

const validateFingerprint = async ({fingerprint, headers}: FingerprintOutput): Promise<boolean> => {
  try {
    // The webdriver attribute should not be truthy
    if (fingerprint.navigator.webdriver) throw new Error('Webdriver attribute is truthy');

    const validUserAgent =
      fingerprint.navigator.userAgent === (headers['user-agent'] ?? headers['User-Agent']);

    // The userAgent should match the one in the headers
    if (!validUserAgent) throw new Error('Invalid user agent');

    const validUserAgentData =
      !fingerprint.navigator.userAgentData ||
      ('brands' in fingerprint.navigator.userAgentData &&
        'mobile' in fingerprint.navigator.userAgentData &&
        'platform' in fingerprint.navigator.userAgentData &&
        fingerprint.navigator.userAgentData.brands.length === 3);

    // The userAgentData should have the correct structure
    if (!validUserAgentData) throw new Error('Invalid user agent data');

    const validLanguage =
      fingerprint.navigator.language &&
      'languages' in fingerprint.navigator &&
      fingerprint.navigator.languages.length > 0 &&
      fingerprint.navigator.language === fingerprint.navigator.languages[0];

    // The language should be the first in the list
    if (!validLanguage) throw new Error('Invalid language');

    const parsedUserAgent: UAParser.IResult = await UAParser(
      fingerprint.navigator.userAgent,
      headers
    ).withClientHints();

    const validBrowser =
      parsedUserAgent.browser.name !== undefined &&
      [
        'Edge',
        'Chrome',
        'Firefox',
        'Safari',
        'Chrome Mobile',
        'Mobile Chrome',
        'Safari Mobile',
        'Mobile Safari',
      ].includes(parsedUserAgent.browser.name);

    // The browser should be one of the supported ones
    if (!validBrowser) throw new Error('Invalid browser');

    const desktopFingerprint =
      parsedUserAgent.device.type === undefined ||
      !['mobile', 'tablet'].includes(parsedUserAgent.device.type);

    const validDeviceType =
      parsedUserAgent.device.type === 'mobile' ||
      parsedUserAgent.device.type === 'tablet' ||
      desktopFingerprint;

    // The device type should be mobile, tablet or desktop
    if (!validDeviceType) throw new Error('Invalid device type');

    const validPluginAndMime =
      !desktopFingerprint ||
      ('plugins' in fingerprint.pluginsData &&
        'mimeTypes' in fingerprint.pluginsData &&
        fingerprint.pluginsData.plugins.length > 0 &&
        fingerprint.pluginsData.mimeTypes.length > 0);

    // The plugins and mimeTypes should be present and non-empty
    if (!validPluginAndMime) throw new Error('Plugins and mimeTypes are missing or empty');

    const validTouchSupport =
      desktopFingerprint ||
      (fingerprint.navigator.userAgentData && fingerprint.navigator.userAgentData.mobile !== true)
        ? fingerprint.navigator.maxTouchPoints === 0
        : fingerprint.navigator.maxTouchPoints > 0;

    // The maxTouchPoints should be 0 for desktops and > 0 for mobile devices
    if (!validTouchSupport) throw new Error('Invalid touch support');

    const validProductSub =
      parsedUserAgent.browser.name === 'Firefox'
        ? fingerprint.navigator.productSub === '20100101'
        : fingerprint.navigator.productSub === '20030107';

    // The productSub should be 20100101 for Firefox and 20030107 for the rest
    if (!validProductSub) throw new Error('Invalid product sub');

    const validVendor =
      (parsedUserAgent.browser.name === 'Firefox' && !fingerprint.navigator.vendor) ||
      (parsedUserAgent.browser.name!.includes('Safari') &&
        fingerprint.navigator.vendor === 'Apple Computer, Inc.') ||
      fingerprint.navigator.vendor === 'Google Inc.';

    // The vendor should be Google Inc. for Chrome and Apple Computer, Inc. for Safari
    if (!validVendor) throw new Error('Invalid vendor');

    const validScreenSize =
      fingerprint.screen.width > 0 &&
      fingerprint.screen.height > 0 &&
      fingerprint.screen.availWidth > 0 &&
      fingerprint.screen.availHeight > 0;

    // The screen dimensions should be positive
    if (!validScreenSize) throw new Error('Invalid screen size');

    const validAvailDimensions =
      fingerprint.screen.availWidth <= fingerprint.screen.width &&
      fingerprint.screen.availHeight <= fingerprint.screen.height;

    // The availWidth and availHeight should be less or equal to the width and height
    if (!validAvailDimensions) throw new Error('Invalid avail dimensions');

    const validWindowSize =
      fingerprint.screen.innerWidth <= fingerprint.screen.outerWidth &&
      fingerprint.screen.innerHeight <= fingerprint.screen.outerHeight;

    // The innerWidth and innerHeight should be less or equal to the outerWidth and outerHeight
    if (!validWindowSize) throw new Error('Invalid window size');

    const validColorDepth =
      !fingerprint.screen.pixelDepth ||
      fingerprint.screen.pixelDepth === fingerprint.screen.colorDepth;

    // The pixelDepth and colorDepth should be equal
    if (!validColorDepth) throw new Error('Invalid color depth');

    const validDevicePixelRatio =
      fingerprint.screen.devicePixelRatio >= 1 && fingerprint.screen.devicePixelRatio <= 5;

    // The devicePixelRatio should be between 1 and 5
    if (!validDevicePixelRatio) throw new Error('Invalid device pixel ratio');

    let commonScreenSize = true;

    if (desktopFingerprint) {
      commonScreenSize =
        fingerprint.screen.width >= 1024 &&
        fingerprint.screen.height >= 768 &&
        fingerprint.screen.availWidth >= 512 &&
        fingerprint.screen.availHeight >= 384;
    } else {
      commonScreenSize =
        fingerprint.screen.width >= 320 &&
        fingerprint.screen.width <= 2560 &&
        fingerprint.screen.height >= 480 &&
        fingerprint.screen.height <= 3200;
    }

    // The screen dimensions should be within the expected range
    if (!commonScreenSize) throw new Error('Invalid screen dimensions');

    return true;
  } catch {
    return false;
  }
};

I’ll be adapting this later today to the flatter record structure so we can get this PR ready to merge!

@barjin
Copy link
Collaborator

barjin commented Jan 28, 2025

Thank you for the work and time you put into this @0xARYA 🙌🏽

I'd still rather see this as a Zod (or similar) schema, so we can just declare the correct shape of the fingerprint and leave the actual validation process to a third-party library. Would you be up to this? If not, I can look into that step - given your work in this PR, the switch actually seems quite simple, so no hard feelings if you want to leave this up to us :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Validate input data before training the models
2 participants