-
Notifications
You must be signed in to change notification settings - Fork 119
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
base: master
Are you sure you want to change the base?
Conversation
…e record structure
…user agent fingerprinting
…agent fingerprinting
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. |
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. |
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. |
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! |
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 :) |
An attempt to close #342