@webdecoy/fcaptcha 1.22.0 → 1.23.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/package.json +1 -1
- package/server.js +48 -5
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1350,6 +1350,19 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1350
1350
|
|
|
1351
1351
|
// Verify PoW if provided
|
|
1352
1352
|
let powValid = false;
|
|
1353
|
+
// powSatisfied records whether the caller actually completed the challenge
|
|
1354
|
+
// this server issued. It gates token issuance below rather than only feeding
|
|
1355
|
+
// the score, because a proof of work is a precondition, not evidence: the
|
|
1356
|
+
// widget solves one on every path and aborts rather than submit without it,
|
|
1357
|
+
// so a request that arrives without a valid solution did not come from the
|
|
1358
|
+
// widget at all.
|
|
1359
|
+
//
|
|
1360
|
+
// Scoring it alone was not enough. The final score is a weighted sum, so the
|
|
1361
|
+
// bot category contributes at most its 0.13 weight — every PoW failure firing
|
|
1362
|
+
// at once reached 0.1298 against a 0.5 threshold, and a bare `curl` with no
|
|
1363
|
+
// solution and no signals was issued a valid token. The detections were all
|
|
1364
|
+
// correct; the aggregation discarded them.
|
|
1365
|
+
let powSatisfied = false;
|
|
1353
1366
|
let powVerification = null;
|
|
1354
1367
|
if (powSolution && powSolution.challengeId) {
|
|
1355
1368
|
powVerification = powChallengeStore.verify(
|
|
@@ -1366,19 +1379,30 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1366
1379
|
category: 'bot',
|
|
1367
1380
|
score: 0.7,
|
|
1368
1381
|
confidence: 0.8,
|
|
1369
|
-
reason: `PoW verification failed: ${powVerification.reason}
|
|
1382
|
+
reason: `PoW verification failed: ${powVerification.reason}`,
|
|
1383
|
+
// A solution that does not verify against a challenge this server
|
|
1384
|
+
// issued is not weak evidence of automation, it is proof the challenge
|
|
1385
|
+
// was not completed. See DISPOSITIVE_FLOOR.
|
|
1386
|
+
dispositive: true
|
|
1370
1387
|
});
|
|
1388
|
+
} else {
|
|
1389
|
+
powSatisfied = true;
|
|
1371
1390
|
}
|
|
1372
1391
|
|
|
1373
1392
|
// Verify challenge nonce binding
|
|
1374
1393
|
if (powValid && powVerification.nonce) {
|
|
1375
1394
|
const clientNonce = signals.meta?.challengeNonce;
|
|
1376
1395
|
if (!clientNonce || clientNonce !== powVerification.nonce) {
|
|
1396
|
+
// The solution verifies but the signals it commits to are not the ones
|
|
1397
|
+
// presented, so the work was done for a different payload. Revokes the
|
|
1398
|
+
// pass granted above.
|
|
1399
|
+
powSatisfied = false;
|
|
1377
1400
|
detections.push({
|
|
1378
1401
|
category: 'bot',
|
|
1379
1402
|
score: 0.9,
|
|
1380
1403
|
confidence: 0.9,
|
|
1381
|
-
reason: 'Challenge nonce mismatch (signals not bound to challenge)'
|
|
1404
|
+
reason: 'Challenge nonce mismatch (signals not bound to challenge)',
|
|
1405
|
+
dispositive: true
|
|
1382
1406
|
});
|
|
1383
1407
|
}
|
|
1384
1408
|
}
|
|
@@ -1410,7 +1434,8 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1410
1434
|
category: 'bot',
|
|
1411
1435
|
score: 0.9,
|
|
1412
1436
|
confidence: 0.95,
|
|
1413
|
-
reason: 'No PoW solution provided'
|
|
1437
|
+
reason: 'No PoW solution provided',
|
|
1438
|
+
dispositive: true
|
|
1414
1439
|
});
|
|
1415
1440
|
}
|
|
1416
1441
|
|
|
@@ -1500,7 +1525,24 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1500
1525
|
// detection layer has nothing to say about it and the refusal is reported as
|
|
1501
1526
|
// its own reason rather than smuggled in as a bot verdict.
|
|
1502
1527
|
const hostnameAllowed = ALLOWED_HOSTNAMES.permits(hostname);
|
|
1503
|
-
|
|
1528
|
+
|
|
1529
|
+
// Three independent conditions, deliberately not folded into the score.
|
|
1530
|
+
//
|
|
1531
|
+
// powSatisfied is the one that matters most: a score threshold answers "how
|
|
1532
|
+
// suspicious is this visitor", which is the wrong question to ask of someone
|
|
1533
|
+
// who never completed the challenge. Gating here means no future reweighting
|
|
1534
|
+
// can reopen the bypass, and it holds even if the dispositive floor is
|
|
1535
|
+
// lowered or removed.
|
|
1536
|
+
const success = finalScore < 0.5 && hostnameAllowed && powSatisfied;
|
|
1537
|
+
|
|
1538
|
+
// Name the failed precondition whenever one fails, not only when the score
|
|
1539
|
+
// would otherwise have allowed. Gating it on the score made the PoW case
|
|
1540
|
+
// unreachable — a PoW failure is dispositive, so it floors the score at 0.9
|
|
1541
|
+
// and the branch never fired — which is exactly the case a caller most needs
|
|
1542
|
+
// explained.
|
|
1543
|
+
let withheldReason = '';
|
|
1544
|
+
if (!powSatisfied) withheldReason = 'pow_not_satisfied';
|
|
1545
|
+
else if (!hostnameAllowed) withheldReason = 'hostname_not_allowed';
|
|
1504
1546
|
|
|
1505
1547
|
const token = success
|
|
1506
1548
|
? generateToken(ip, siteKey, finalScore, {
|
|
@@ -1522,7 +1564,8 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1522
1564
|
recommendation,
|
|
1523
1565
|
categoryScores,
|
|
1524
1566
|
detections,
|
|
1525
|
-
...(
|
|
1567
|
+
...(withheldReason ? { reason: withheldReason } : {}),
|
|
1568
|
+
...(hostnameAllowed ? {} : { hostname })
|
|
1526
1569
|
};
|
|
1527
1570
|
}
|
|
1528
1571
|
|