@webdecoy/fcaptcha 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/detection.js +321 -1
- package/package.json +1 -1
- package/server.js +4 -0
package/detection.js
CHANGED
|
@@ -431,6 +431,317 @@ function analyzeFormInteraction(formAnalysis) {
|
|
|
431
431
|
return detections;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
// =============================================================================
|
|
435
|
+
// Advanced Fingerprint Detection Functions
|
|
436
|
+
// =============================================================================
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Analyze WebRTC signals for headless browser and VM detection
|
|
440
|
+
*/
|
|
441
|
+
function analyzeWebRTC(webrtcInfo) {
|
|
442
|
+
if (!webrtcInfo || !webrtcInfo.supported) return [];
|
|
443
|
+
|
|
444
|
+
const detections = [];
|
|
445
|
+
|
|
446
|
+
// Check media devices - headless browsers typically have 0 devices
|
|
447
|
+
const mediaDevices = webrtcInfo.mediaDevices || {};
|
|
448
|
+
if (mediaDevices.supported && mediaDevices.totalDevices === 0) {
|
|
449
|
+
detections.push({
|
|
450
|
+
category: 'headless',
|
|
451
|
+
score: 0.7,
|
|
452
|
+
confidence: 0.75,
|
|
453
|
+
reason: 'No media devices detected (typical of headless browsers)'
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Suspicious: has video inputs but no audio (unusual)
|
|
458
|
+
if (mediaDevices.supported && mediaDevices.videoInputs > 0 && mediaDevices.audioInputs === 0) {
|
|
459
|
+
detections.push({
|
|
460
|
+
category: 'bot',
|
|
461
|
+
score: 0.4,
|
|
462
|
+
confidence: 0.5,
|
|
463
|
+
reason: 'Has video devices but no audio devices (unusual configuration)'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check local IP detection - VMs and some headless setups may not expose local IPs
|
|
468
|
+
if (webrtcInfo.hasLocalIP === false && !webrtcInfo.localIPError) {
|
|
469
|
+
detections.push({
|
|
470
|
+
category: 'headless',
|
|
471
|
+
score: 0.4,
|
|
472
|
+
confidence: 0.5,
|
|
473
|
+
reason: 'No local IP addresses detected via WebRTC'
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return detections;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Analyze Speech API signals - voices are OS/browser specific and hard to spoof
|
|
482
|
+
*/
|
|
483
|
+
function analyzeSpeechAPI(speechInfo) {
|
|
484
|
+
if (!speechInfo || !speechInfo.supported) return [];
|
|
485
|
+
|
|
486
|
+
const detections = [];
|
|
487
|
+
|
|
488
|
+
// No voices at all - suspicious
|
|
489
|
+
if (speechInfo.totalVoices === 0) {
|
|
490
|
+
detections.push({
|
|
491
|
+
category: 'headless',
|
|
492
|
+
score: 0.6,
|
|
493
|
+
confidence: 0.7,
|
|
494
|
+
reason: 'No speech synthesis voices available'
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Very few voices (less than 5) - could be headless or minimal VM
|
|
499
|
+
if (speechInfo.totalVoices > 0 && speechInfo.totalVoices < 5) {
|
|
500
|
+
detections.push({
|
|
501
|
+
category: 'headless',
|
|
502
|
+
score: 0.3,
|
|
503
|
+
confidence: 0.4,
|
|
504
|
+
reason: `Very few speech voices available (${speechInfo.totalVoices})`
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// No local voices - all remote/network voices suggests unusual setup
|
|
509
|
+
if (speechInfo.localVoices === 0 && speechInfo.totalVoices > 0) {
|
|
510
|
+
detections.push({
|
|
511
|
+
category: 'bot',
|
|
512
|
+
score: 0.3,
|
|
513
|
+
confidence: 0.4,
|
|
514
|
+
reason: 'No local speech synthesis voices'
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return detections;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Analyze Worker consistency - spoofed values often don't match between contexts
|
|
523
|
+
*/
|
|
524
|
+
function analyzeWorkerConsistency(workerConsistency) {
|
|
525
|
+
if (!workerConsistency || !workerConsistency.supported) return [];
|
|
526
|
+
|
|
527
|
+
const detections = [];
|
|
528
|
+
|
|
529
|
+
// Mismatches indicate fingerprint spoofing
|
|
530
|
+
if (!workerConsistency.consistent && workerConsistency.mismatchCount > 0) {
|
|
531
|
+
const score = Math.min(0.9, 0.3 + (workerConsistency.mismatchCount * 0.15));
|
|
532
|
+
detections.push({
|
|
533
|
+
category: 'bot',
|
|
534
|
+
score: score,
|
|
535
|
+
confidence: 0.85,
|
|
536
|
+
reason: `Worker/main thread mismatch detected: ${workerConsistency.mismatches.join(', ')}`
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return detections;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Analyze CSS Media Queries for environment consistency
|
|
545
|
+
*/
|
|
546
|
+
function analyzeCSSMediaQueries(cssMedia, signals) {
|
|
547
|
+
if (!cssMedia || !cssMedia.supported) return [];
|
|
548
|
+
|
|
549
|
+
const detections = [];
|
|
550
|
+
const nav = (signals.environmental || {}).navigator || {};
|
|
551
|
+
|
|
552
|
+
// Check pointer consistency with touch capability
|
|
553
|
+
const maxTouch = nav.maxTouchPoints || 0;
|
|
554
|
+
if (cssMedia.pointer === 'coarse' && maxTouch === 0) {
|
|
555
|
+
detections.push({
|
|
556
|
+
category: 'bot',
|
|
557
|
+
score: 0.5,
|
|
558
|
+
confidence: 0.6,
|
|
559
|
+
reason: 'CSS reports coarse pointer but no touch support'
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check hover capability - headless browsers may have unusual values
|
|
564
|
+
if (cssMedia.hover === false && cssMedia.pointer === 'fine') {
|
|
565
|
+
detections.push({
|
|
566
|
+
category: 'bot',
|
|
567
|
+
score: 0.3,
|
|
568
|
+
confidence: 0.4,
|
|
569
|
+
reason: 'Fine pointer reported but no hover capability'
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Forced colors mode with reduced motion - accessibility features
|
|
574
|
+
// Note: These are NOT suspicious on their own, just for fingerprinting
|
|
575
|
+
// Don't penalize accessibility users
|
|
576
|
+
|
|
577
|
+
return detections;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Analyze font detection results for consistency
|
|
582
|
+
*/
|
|
583
|
+
function analyzeFonts(fontsInfo, userAgent) {
|
|
584
|
+
if (!fontsInfo || !fontsInfo.supported) return [];
|
|
585
|
+
|
|
586
|
+
const detections = [];
|
|
587
|
+
|
|
588
|
+
// Very few fonts detected could indicate headless browser
|
|
589
|
+
if (fontsInfo.count < 3) {
|
|
590
|
+
detections.push({
|
|
591
|
+
category: 'headless',
|
|
592
|
+
score: 0.5,
|
|
593
|
+
confidence: 0.5,
|
|
594
|
+
reason: `Very few fonts detected (${fontsInfo.count})`
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check OS-specific fonts against UA
|
|
599
|
+
const ua = (userAgent || '').toLowerCase();
|
|
600
|
+
|
|
601
|
+
// Windows UA but no Segoe UI
|
|
602
|
+
if (ua.includes('windows') && fontsInfo.hasSegoeUI === false && fontsInfo.count > 5) {
|
|
603
|
+
detections.push({
|
|
604
|
+
category: 'bot',
|
|
605
|
+
score: 0.5,
|
|
606
|
+
confidence: 0.6,
|
|
607
|
+
reason: 'Windows UA but Segoe UI font not detected'
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Mac UA but no SF Pro (modern macOS)
|
|
612
|
+
if ((ua.includes('mac os x') || ua.includes('macintosh')) && fontsInfo.hasSFPro === false &&
|
|
613
|
+
ua.includes('10_15') === false && ua.includes('10_14') === false && fontsInfo.count > 5) {
|
|
614
|
+
// SF Pro is on macOS 10.15+ (Catalina and later)
|
|
615
|
+
detections.push({
|
|
616
|
+
category: 'bot',
|
|
617
|
+
score: 0.3,
|
|
618
|
+
confidence: 0.4,
|
|
619
|
+
reason: 'Modern macOS UA but SF Pro font not detected'
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Linux UA but no DejaVu Sans (very common on Linux)
|
|
624
|
+
if (ua.includes('linux') && !ua.includes('android') &&
|
|
625
|
+
fontsInfo.hasDejaVuSans === false && fontsInfo.count > 5) {
|
|
626
|
+
detections.push({
|
|
627
|
+
category: 'bot',
|
|
628
|
+
score: 0.4,
|
|
629
|
+
confidence: 0.5,
|
|
630
|
+
reason: 'Linux UA but DejaVu Sans font not detected'
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return detections;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Analyze permissions/API availability for headless detection
|
|
639
|
+
*/
|
|
640
|
+
function analyzePermissions(permissionsInfo) {
|
|
641
|
+
if (!permissionsInfo || !permissionsInfo.supported) return [];
|
|
642
|
+
|
|
643
|
+
const detections = [];
|
|
644
|
+
|
|
645
|
+
// Count available APIs
|
|
646
|
+
const apiKeys = [
|
|
647
|
+
'hasPermissionsAPI', 'hasClipboard', 'hasShare', 'hasCredentials',
|
|
648
|
+
'hasBluetooth', 'hasUsb', 'hasSerial', 'hasHid', 'hasXR',
|
|
649
|
+
'hasGeolocation', 'hasMIDI'
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
const availableApis = apiKeys.filter(key => permissionsInfo[key] === true).length;
|
|
653
|
+
|
|
654
|
+
// Very few APIs available could indicate minimal/headless browser
|
|
655
|
+
if (availableApis < 3) {
|
|
656
|
+
detections.push({
|
|
657
|
+
category: 'headless',
|
|
658
|
+
score: 0.4,
|
|
659
|
+
confidence: 0.5,
|
|
660
|
+
reason: `Very few navigator APIs available (${availableApis})`
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return detections;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Analyze DOMRect/geometry fingerprint for rendering anomalies
|
|
669
|
+
*/
|
|
670
|
+
function analyzeDOMRect(domRectInfo) {
|
|
671
|
+
if (!domRectInfo || !domRectInfo.supported) return [];
|
|
672
|
+
|
|
673
|
+
const detections = [];
|
|
674
|
+
|
|
675
|
+
// Check for zero or very small dimensions (rendering issues)
|
|
676
|
+
if (domRectInfo.rectAWidth === 0 || domRectInfo.rectBWidth === 0) {
|
|
677
|
+
detections.push({
|
|
678
|
+
category: 'headless',
|
|
679
|
+
score: 0.6,
|
|
680
|
+
confidence: 0.7,
|
|
681
|
+
reason: 'DOMRect rendering returned zero-width elements'
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Check for exact integer values (unusual in real browsers)
|
|
686
|
+
if (domRectInfo.rectAWidth === Math.floor(domRectInfo.rectAWidth) &&
|
|
687
|
+
domRectInfo.rectBWidth === Math.floor(domRectInfo.rectBWidth) &&
|
|
688
|
+
domRectInfo.rangeWidth === Math.floor(domRectInfo.rangeWidth)) {
|
|
689
|
+
detections.push({
|
|
690
|
+
category: 'bot',
|
|
691
|
+
score: 0.3,
|
|
692
|
+
confidence: 0.4,
|
|
693
|
+
reason: 'DOMRect measurements are all exact integers (unusual)'
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return detections;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Master function to analyze all advanced fingerprint signals
|
|
702
|
+
*/
|
|
703
|
+
function analyzeAdvancedSignals(signals, userAgent) {
|
|
704
|
+
const detections = [];
|
|
705
|
+
const env = signals.environmental || {};
|
|
706
|
+
|
|
707
|
+
// WebRTC analysis
|
|
708
|
+
if (env.webrtcInfo) {
|
|
709
|
+
detections.push(...analyzeWebRTC(env.webrtcInfo));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Speech API analysis
|
|
713
|
+
if (env.speechInfo) {
|
|
714
|
+
detections.push(...analyzeSpeechAPI(env.speechInfo));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Worker consistency analysis
|
|
718
|
+
if (env.workerConsistency) {
|
|
719
|
+
detections.push(...analyzeWorkerConsistency(env.workerConsistency));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// CSS Media Queries analysis
|
|
723
|
+
if (env.cssMediaQueries) {
|
|
724
|
+
detections.push(...analyzeCSSMediaQueries(env.cssMediaQueries, signals));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Font analysis
|
|
728
|
+
if (env.fontsInfo) {
|
|
729
|
+
detections.push(...analyzeFonts(env.fontsInfo, userAgent));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Permissions analysis
|
|
733
|
+
if (env.permissionsInfo) {
|
|
734
|
+
detections.push(...analyzePermissions(env.permissionsInfo));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// DOMRect analysis
|
|
738
|
+
if (env.domRectFingerprint) {
|
|
739
|
+
detections.push(...analyzeDOMRect(env.domRectFingerprint));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return detections;
|
|
743
|
+
}
|
|
744
|
+
|
|
434
745
|
module.exports = {
|
|
435
746
|
isDatacenterIP,
|
|
436
747
|
checkIPReputation,
|
|
@@ -438,5 +749,14 @@ module.exports = {
|
|
|
438
749
|
parseUserAgent,
|
|
439
750
|
checkBrowserConsistency,
|
|
440
751
|
checkJA3Fingerprint,
|
|
441
|
-
analyzeFormInteraction
|
|
752
|
+
analyzeFormInteraction,
|
|
753
|
+
// Advanced fingerprint detection functions
|
|
754
|
+
analyzeWebRTC,
|
|
755
|
+
analyzeSpeechAPI,
|
|
756
|
+
analyzeWorkerConsistency,
|
|
757
|
+
analyzeCSSMediaQueries,
|
|
758
|
+
analyzeFonts,
|
|
759
|
+
analyzePermissions,
|
|
760
|
+
analyzeDOMRect,
|
|
761
|
+
analyzeAdvancedSignals
|
|
442
762
|
};
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -777,6 +777,10 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
777
777
|
detections.push(...formDetections);
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// Add advanced fingerprint detection analysis
|
|
781
|
+
const advancedDetections = detection.analyzeAdvancedSignals(signals, userAgent);
|
|
782
|
+
detections.push(...advancedDetections);
|
|
783
|
+
|
|
780
784
|
const categoryScores = calculateCategoryScores(detections);
|
|
781
785
|
const finalScore = calculateFinalScore(categoryScores);
|
|
782
786
|
|