@@ -296,112 +296,156 @@ let sync_metrics = {
296296 return rp ;
297297 } ) ( ) ,
298298
299- /**
300- * Media devices
301- * https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
302- */
303- media_devices : {
304- navigator_mediaDevices_enumerateDevices : testPropertyStringInResponseBodies (
305- 'mediaDevices.+enumerateDevices'
306- ) ,
307- navigator_mediaDevices_getUserMedia : testPropertyStringInResponseBodies (
308- 'mediaDevices.+getUserMedia'
309- ) ,
310- navigator_mediaDevices_getDisplayMedia : testPropertyStringInResponseBodies (
311- 'mediaDevices.+getDisplayMedia'
312- ) ,
313- } ,
314-
315- /**
316- * Geolocation API
317- * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API
318- */
319- geolocation : {
320- navigator_geolocation_getCurrentPosition : testPropertyStringInResponseBodies (
321- 'geolocation.+getCurrentPosition'
322- ) ,
323- navigator_geolocation_watchPosition : testPropertyStringInResponseBodies (
324- 'geolocation.+watchPosition'
325- ) ,
326- } ,
327-
328299 fingerprinting : ( ( ) => {
329- //These are determined by looking at the tests in https://github.com/fingerprintjs/fingerprintjs
300+ // These are determined by looking at the tests in https://github.com/fingerprintjs/fingerprintjs
330301 const fingerprintingAPIs = [
331- 'ApplePaySession.canMakePayments' ,
332- 'getChannelData' , //audioContext
333- 'toDataURL' , //canvas
334- 'getImageData' , //canvas, not actually used by fingerprintJS
335- 'screen.colorDepth' ,
336- 'color-gamut' ,
337- 'prefers-contrast' ,
302+ // Payment APIs
303+ 'ApplePaySession\\.canMakePayments' ,
304+
305+ // Audio fingerprinting
306+ 'createAnalyser' ,
307+ 'createOscillator' ,
308+ 'createScriptProcessor' ,
309+ 'getChannelData' ,
310+ 'getFloatFrequencyData' ,
311+ 'getByteFrequencyData' ,
312+ 'OscillatorNode' ,
313+
314+ // Canvas fingerprinting
315+ 'canvas\\.getContext' ,
316+ 'canvas\\.toDataURL' ,
317+ 'canvasRenderingContext2D\\.fillText' ,
318+ 'canvasRenderingContext2D\\.strokeText' ,
319+ 'canvasRenderingContext2D\\.getImageData' ,
320+ 'HTMLCanvasElement\\.toBlob' ,
321+
322+ // CSS media queries for fingerprinting
323+ '@media.*color-gamut' ,
324+ '@media.*prefers-contrast' ,
325+ '@media.*forced-colors' ,
326+ '@media.*dynamic-range' ,
327+ '@media.*inverted-colors' ,
328+ '@media.*min-monochrome' ,
329+ '@media.*max-monochrome' ,
330+ '@media.*prefers-reduced-motion' ,
331+ '@media.*prefers-reduced-transparency' ,
332+
333+ // Hardware fingerprinting
338334 'cpuClass' ,
339335 'deviceMemory' ,
340- 'forced-colors' ,
341336 'hardwareConcurrency' ,
342- 'dynamic-range' ,
337+ 'maxTouchPoints' ,
338+ 'ontouchstart' ,
339+
340+ // Storage APIs (potential fingerprinting)
343341 'indexedDB' ,
344- 'inverted-colors' ,
345- 'navigator.language' , //"language" would be too generic here
346- 'navigator.userLanguage' , //TODO exists?
347342 'localStorage' ,
348- 'min-monochrome' ,
349- 'max-monochrome' ,
343+ 'sessionStorage' ,
350344 'openDatabase' ,
351- 'navigator.oscpu' ,
345+
346+ // PDF and plugins
352347 'pdfViewerEnabled' ,
353- 'navigator.platform' , //"platform" would be too generic
354- 'navigator.plugins' ,
348+
349+ // Attribution and tracking
355350 'attributionSourceId' ,
356- 'prefers-reduced-motion' ,
357- 'prefers-reduced-transparency' ,
358- 'availWidth' ,
359- 'availHeight' ,
360- 'screen.width' ,
361- 'screen.height' ,
362- 'sessionStorage' ,
363- 'resolvedOptions().timeZone' ,
351+
352+ // Time zone fingerprinting
353+ 'resolvedOptions\\(\\)\\.timeZone' ,
364354 'getTimezoneOffset' ,
365- 'maxTouchPoints' ,
366- 'ontouchstart' ,
367- 'navigator.vendor' ,
355+
356+ // WebGL fingerprinting
368357 'vendorUnmasked' ,
369358 'rendererUnmasked' ,
370359 'shadingLanguageVersion' ,
371360 'WEBGL_debug_renderer_info' ,
372- 'getShaderPrecisionFormat'
373- ] . map ( api => api . toLowerCase ( ) )
361+ 'getShaderPrecisionFormat' ,
374362
375- const response_bodies = $WPT_BODIES . filter ( body => ( body . response_body && ( body . type === 'Document' || body . type === 'Script' ) ) )
376-
377- let fingerprintingUsageCounts = { }
378- let likelyFingerprintingScripts = [ ]
363+ // Screen properties
364+ 'availWidth' ,
365+ 'availHeight' ,
366+ 'screen\\.width' ,
367+ 'screen\\.height' ,
368+ 'screen\\.colorDepth' ,
369+
370+ // Navigator properties
371+ 'navigator\\.platform' ,
372+ 'navigator\\.plugins' ,
373+ 'navigator\\.language' ,
374+ 'navigator\\.oscpu' ,
375+ 'navigator\\.vendor' ,
376+ 'navigator\\.getBattery' ,
377+ 'navigator\\.getGamepads' ,
378+
379+ // Geolocation API: https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API
380+ 'getCurrentPosition' ,
381+ 'watchPosition' ,
382+
383+ // Media devices: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
384+ 'enumerateDevices' ,
385+ 'getUserMedia' ,
386+ 'getDisplayMedia' ,
387+
388+ // Additional modern fingerprinting vectors
389+ 'RTCPeerConnection' ,
390+ 'document\\.fonts' ,
391+ 'performance\\.memory' ,
392+ ] ;
393+
394+ // Pre-compile regexes - handle already escaped patterns
395+ const compiledRegexes = fingerprintingAPIs . map ( api => ( {
396+ api,
397+ regex : new RegExp ( api , 'gi' )
398+ } ) ) ;
399+ let likelyFingerprintingScripts = [ ] ;
379400
380401 response_bodies . forEach ( req => {
381- let total_occurrences = 0
382-
383- let body = req . response_body . toLowerCase ( )
402+ try {
403+ let detectedApis = [ ] ;
404+ let totalOccurrences = 0 ;
405+
406+ compiledRegexes . forEach ( ( { api, regex } ) => {
407+ try {
408+ // Reset regex index for global regex
409+ regex . lastIndex = 0 ;
410+
411+ // Use a more memory-efficient counting approach
412+ let match ;
413+ let matches = 0 ;
414+ while ( ( match = regex . exec ( req . response_body ) ) !== null ) {
415+ matches ++ ;
416+ // Prevent infinite loops on zero-length matches
417+ if ( match . index === regex . lastIndex ) {
418+ regex . lastIndex ++ ;
419+ }
420+ }
384421
385- fingerprintingAPIs . forEach ( api => {
386- let api_occurrences = 0
387- let index = body . indexOf ( api )
388- while ( index !== - 1 ) {
389- api_occurrences ++
390- index = body . indexOf ( api , index + 1 )
391- }
422+ if ( matches > 0 ) {
423+ detectedApis . push ( api ) ;
424+ totalOccurrences += matches ;
425+ }
426+ } catch ( regexError ) {
427+ // Skip this API on regex error - avoid console.warn in WebPageTest
428+ }
429+ } ) ;
392430
393- if ( api_occurrences > 0 ) {
394- fingerprintingUsageCounts [ api ] = ( fingerprintingUsageCounts [ api ] || 0 ) + api_occurrences
431+ // Track scripts with significant fingerprinting API usage (threshold: 4+ APIs or high occurrence count)
432+ const suspicionScore = Math . round ( ( detectedApis . length * 2 + totalOccurrences ) / 3 ) ;
433+ if ( detectedApis . length >= 4 || suspicionScore >= 8 ) {
434+ likelyFingerprintingScripts . push ( {
435+ url : req . url ,
436+ detectedApis : detectedApis ,
437+ suspicionScore : suspicionScore
438+ } ) ;
395439 }
396- total_occurrences += api_occurrences
397- } )
398-
399- if ( total_occurrences >= 5 ) { //TODO what should this threshold be?
400- likelyFingerprintingScripts . push ( req . url )
440+ } catch ( error ) {
441+ // Skip this request on error - avoid console.warn in WebPageTest
401442 }
402- } )
443+ } ) ;
444+
445+ // Sort by suspicion score (highest first)
446+ likelyFingerprintingScripts . sort ( ( a , b ) => b . suspicionScore - a . suspicionScore ) ;
403447
404- return { counts : fingerprintingUsageCounts , likelyFingerprintingScripts }
448+ return likelyFingerprintingScripts ;
405449 } ) ( ) ,
406450
407451 /**
0 commit comments