@webdecoy/fcaptcha 1.0.1 → 1.0.4
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/package.json +1 -1
- package/server.js +106 -9
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -42,13 +42,11 @@ const powChallengeStore = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// Sign the challenge
|
|
45
|
-
|
|
45
|
+
challengeData.sig = crypto.createHmac('sha256', SECRET_KEY)
|
|
46
46
|
.update(JSON.stringify(challengeData))
|
|
47
47
|
.digest('hex')
|
|
48
48
|
.slice(0, 16);
|
|
49
49
|
|
|
50
|
-
challengeData.sig = sig;
|
|
51
|
-
|
|
52
50
|
// Store challenge
|
|
53
51
|
this.challenges.set(challengeId, {
|
|
54
52
|
...challengeData,
|
|
@@ -173,6 +171,35 @@ const fingerprintStore = {
|
|
|
173
171
|
}
|
|
174
172
|
};
|
|
175
173
|
|
|
174
|
+
// Token Store - prevents token replay attacks
|
|
175
|
+
const tokenStore = {
|
|
176
|
+
usedTokens: new Set(),
|
|
177
|
+
|
|
178
|
+
// Mark a token as used (returns false if already used)
|
|
179
|
+
markUsed(tokenSig) {
|
|
180
|
+
if (this.usedTokens.has(tokenSig)) {
|
|
181
|
+
return false; // Already used
|
|
182
|
+
}
|
|
183
|
+
this.usedTokens.add(tokenSig);
|
|
184
|
+
|
|
185
|
+
// Cleanup old tokens periodically (tokens expire after 5 min anyway)
|
|
186
|
+
if (Math.random() < 0.1) this._cleanup();
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
isUsed(tokenSig) {
|
|
191
|
+
return this.usedTokens.has(tokenSig);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
_cleanup() {
|
|
195
|
+
// In production with Redis, use TTL instead
|
|
196
|
+
// For in-memory, just clear if too large (tokens expire in 5 min)
|
|
197
|
+
if (this.usedTokens.size > 50000) {
|
|
198
|
+
this.usedTokens.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
176
203
|
// =============================================================================
|
|
177
204
|
// Detection Patterns
|
|
178
205
|
// =============================================================================
|
|
@@ -187,7 +214,8 @@ const WEIGHTS = {
|
|
|
187
214
|
vision_ai: 0.15,
|
|
188
215
|
headless: 0.15,
|
|
189
216
|
automation: 0.10,
|
|
190
|
-
|
|
217
|
+
cdp: 0.12,
|
|
218
|
+
behavioral: 0.18,
|
|
191
219
|
fingerprint: 0.10,
|
|
192
220
|
rate_limit: 0.05,
|
|
193
221
|
datacenter: 0.10,
|
|
@@ -383,6 +411,46 @@ function detectAutomation(signals) {
|
|
|
383
411
|
return detections;
|
|
384
412
|
}
|
|
385
413
|
|
|
414
|
+
function detectCDP(signals) {
|
|
415
|
+
const detections = [];
|
|
416
|
+
const env = signals.environmental || {};
|
|
417
|
+
const cdp = env.cdp || {};
|
|
418
|
+
|
|
419
|
+
if (cdp.detected) {
|
|
420
|
+
const signalList = cdp.signals || [];
|
|
421
|
+
const signalCount = signalList.length;
|
|
422
|
+
|
|
423
|
+
// High-confidence signals
|
|
424
|
+
const highConfSignals = ['chromedriver_cdc', 'puppeteer_eval', 'cdp_script_injection'];
|
|
425
|
+
const hasHighConf = signalList.some(s => highConfSignals.includes(s));
|
|
426
|
+
|
|
427
|
+
if (hasHighConf) {
|
|
428
|
+
detections.push({
|
|
429
|
+
category: 'cdp',
|
|
430
|
+
score: 0.9,
|
|
431
|
+
confidence: 0.95,
|
|
432
|
+
reason: `CDP automation detected: ${signalList.join(', ')}`
|
|
433
|
+
});
|
|
434
|
+
} else if (signalCount >= 2) {
|
|
435
|
+
detections.push({
|
|
436
|
+
category: 'cdp',
|
|
437
|
+
score: 0.8,
|
|
438
|
+
confidence: 0.85,
|
|
439
|
+
reason: `Multiple CDP indicators: ${signalList.join(', ')}`
|
|
440
|
+
});
|
|
441
|
+
} else if (signalCount === 1) {
|
|
442
|
+
detections.push({
|
|
443
|
+
category: 'cdp',
|
|
444
|
+
score: 0.6,
|
|
445
|
+
confidence: 0.7,
|
|
446
|
+
reason: `CDP indicator: ${signalList.join(', ')}`
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return detections;
|
|
452
|
+
}
|
|
453
|
+
|
|
386
454
|
function detectBehavioral(signals) {
|
|
387
455
|
const detections = [];
|
|
388
456
|
const b = signals.behavioral || {};
|
|
@@ -585,13 +653,12 @@ function generateToken(ip, siteKey, score) {
|
|
|
585
653
|
};
|
|
586
654
|
|
|
587
655
|
const payload = JSON.stringify(data, Object.keys(data).sort());
|
|
588
|
-
|
|
589
|
-
data.sig = sig;
|
|
656
|
+
data.sig = crypto.createHmac('sha256', SECRET_KEY).update(payload).digest('hex').slice(0, 16);
|
|
590
657
|
|
|
591
658
|
return Buffer.from(JSON.stringify(data)).toString('base64url');
|
|
592
659
|
}
|
|
593
660
|
|
|
594
|
-
function verifyToken(token) {
|
|
661
|
+
function verifyToken(token, ip = null) {
|
|
595
662
|
try {
|
|
596
663
|
const decoded = JSON.parse(Buffer.from(token, 'base64url').toString());
|
|
597
664
|
|
|
@@ -610,11 +677,28 @@ function verifyToken(token) {
|
|
|
610
677
|
return { valid: false, reason: 'invalid_signature' };
|
|
611
678
|
}
|
|
612
679
|
|
|
680
|
+
// Check for token replay (single-use tokens)
|
|
681
|
+
if (tokenStore.isUsed(sig)) {
|
|
682
|
+
return { valid: false, reason: 'token_already_used' };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Verify IP matches (if provided)
|
|
686
|
+
if (ip) {
|
|
687
|
+
const expectedIpHash = crypto.createHash('sha256').update(ip).digest('hex').slice(0, 8);
|
|
688
|
+
if (decoded.ip_hash !== expectedIpHash) {
|
|
689
|
+
return { valid: false, reason: 'ip_mismatch' };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Mark token as used (prevents replay)
|
|
694
|
+
tokenStore.markUsed(sig);
|
|
695
|
+
|
|
613
696
|
return {
|
|
614
697
|
valid: true,
|
|
615
698
|
site_key: decoded.site_key,
|
|
616
699
|
timestamp: decoded.timestamp,
|
|
617
|
-
score: decoded.score
|
|
700
|
+
score: decoded.score,
|
|
701
|
+
ip_hash: decoded.ip_hash
|
|
618
702
|
};
|
|
619
703
|
} catch (e) {
|
|
620
704
|
return { valid: false, reason: e.message };
|
|
@@ -626,6 +710,7 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
626
710
|
...detectVisionAI(signals),
|
|
627
711
|
...detectHeadless(signals, userAgent),
|
|
628
712
|
...detectAutomation(signals),
|
|
713
|
+
...detectCDP(signals),
|
|
629
714
|
...detectBehavioral(signals),
|
|
630
715
|
...detectFingerprint(signals, ip, siteKey),
|
|
631
716
|
...detectRateAbuse(ip, siteKey)
|
|
@@ -777,7 +862,19 @@ app.post('/api/score', (req, res) => {
|
|
|
777
862
|
|
|
778
863
|
app.post('/api/token/verify', (req, res) => {
|
|
779
864
|
const { token } = req.body;
|
|
780
|
-
|
|
865
|
+
|
|
866
|
+
// Extract client IP for verification
|
|
867
|
+
let ip = req.headers['x-real-ip'] || '';
|
|
868
|
+
if (!ip) {
|
|
869
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
870
|
+
if (forwarded) {
|
|
871
|
+
ip = forwarded.split(',')[0].trim();
|
|
872
|
+
} else {
|
|
873
|
+
ip = req.socket.remoteAddress;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
res.json(verifyToken(token, ip));
|
|
781
878
|
});
|
|
782
879
|
|
|
783
880
|
// PoW Challenge endpoint - client fetches this on page load
|