@webdecoy/fcaptcha 1.28.2 → 1.30.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/config.js +22 -0
- package/index.js +3 -2
- package/package.json +2 -2
- package/server.js +22 -8
package/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const INSECURE_DEFAULT_SECRET = 'dev-secret-change-in-production';
|
|
4
|
+
|
|
5
|
+
function signingSecretFromEnv(env = process.env) {
|
|
6
|
+
const secret = String(env.FCAPTCHA_SECRET || '').trim();
|
|
7
|
+
if (secret && secret !== INSECURE_DEFAULT_SECRET) return secret;
|
|
8
|
+
if (/^(1|true|yes|on)$/i.test(String(env.FCAPTCHA_INSECURE_DEV_MODE || '').trim())) {
|
|
9
|
+
console.warn('WARNING: FCAPTCHA_INSECURE_DEV_MODE enabled; tokens use a public signing key. Never expose this server to a network.');
|
|
10
|
+
return INSECURE_DEFAULT_SECRET;
|
|
11
|
+
}
|
|
12
|
+
throw new Error('FCAPTCHA_SECRET is required and must not be the public development key. For local-only development, explicitly set FCAPTCHA_INSECURE_DEV_MODE=1.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function signingSecret(explicit, env = process.env) {
|
|
16
|
+
if (explicit !== undefined && explicit !== null) {
|
|
17
|
+
return signingSecretFromEnv({ ...env, FCAPTCHA_SECRET: explicit });
|
|
18
|
+
}
|
|
19
|
+
return signingSecretFromEnv(env);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { INSECURE_DEFAULT_SECRET, signingSecret, signingSecretFromEnv };
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const crypto = require('crypto');
|
|
|
13
13
|
const detection = require('./detection');
|
|
14
14
|
const { ProxyTrust } = require('./clientip');
|
|
15
15
|
const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./suspicion');
|
|
16
|
+
const { signingSecret } = require('./config');
|
|
16
17
|
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// PoW Challenge Store (can be extended with Redis)
|
|
@@ -20,7 +21,7 @@ const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./su
|
|
|
20
21
|
|
|
21
22
|
class PoWChallengeStore {
|
|
22
23
|
constructor(options = {}) {
|
|
23
|
-
this.secret = options.secret
|
|
24
|
+
this.secret = signingSecret(options.secret);
|
|
24
25
|
this.challenges = new Map();
|
|
25
26
|
this.usedSolutions = new Set();
|
|
26
27
|
this.expirationMs = options.expirationMs || 5 * 60 * 1000; // 5 minutes
|
|
@@ -212,7 +213,7 @@ const AUTOMATION_UA_PATTERNS = [
|
|
|
212
213
|
|
|
213
214
|
class ScoringEngine {
|
|
214
215
|
constructor(options = {}) {
|
|
215
|
-
this.secret = options.secret
|
|
216
|
+
this.secret = signingSecret(options.secret);
|
|
216
217
|
this.powStore = options.powStore || new PoWChallengeStore({ secret: this.secret });
|
|
217
218
|
this.rateLimiter = options.rateLimiter || new RateLimiter();
|
|
218
219
|
this.fingerprintStore = options.fingerprintStore || new FingerprintStore();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webdecoy/fcaptcha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Open source CAPTCHA with PoW, bot detection, and Vision AI protection",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test:clientip": "node clientip.test.js",
|
|
16
16
|
"test:limits": "node limits.test.js",
|
|
17
17
|
"test:detection": "node detection.test.js",
|
|
18
|
-
"test": "node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node index.test.js && node --test suspicion.test.js",
|
|
18
|
+
"test": "node config.test.js && node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node index.test.js && node --test suspicion.test.js",
|
|
19
19
|
"test:siteverify": "node siteverify.test.js",
|
|
20
20
|
"test:forensics": "node inputforensics.test.js",
|
|
21
21
|
"test:suspicion": "node --test suspicion.test.js"
|
package/server.js
CHANGED
|
@@ -12,6 +12,7 @@ const detection = require('./detection');
|
|
|
12
12
|
const webbotauth = require('./webbotauth');
|
|
13
13
|
const { ProxyTrust } = require('./clientip');
|
|
14
14
|
const { BoundedMap, BoundedSet, SiteKeyGuard } = require('./limits');
|
|
15
|
+
const { signingSecretFromEnv } = require('./config');
|
|
15
16
|
const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./suspicion');
|
|
16
17
|
const { detectInputForensics } = require('./inputforensics');
|
|
17
18
|
const {
|
|
@@ -27,13 +28,16 @@ const {
|
|
|
27
28
|
|
|
28
29
|
const app = express();
|
|
29
30
|
app.use(cors());
|
|
30
|
-
|
|
31
|
+
// Captured widget payloads are about 10-15 KiB. Four times that leaves ample
|
|
32
|
+
// room for future signals while bounding work done by the public JSON parsers.
|
|
33
|
+
const MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
34
|
+
app.use(express.json({ limit: MAX_REQUEST_BODY_BYTES }));
|
|
31
35
|
// The siteverify contract accepts form-encoded bodies as well as JSON, and most
|
|
32
36
|
// PHP and Python integrations in the wild post form-encoded. Parsing both here
|
|
33
37
|
// costs nothing on the JSON path.
|
|
34
|
-
app.use(express.urlencoded({ extended: false }));
|
|
38
|
+
app.use(express.urlencoded({ extended: false, limit: MAX_REQUEST_BODY_BYTES }));
|
|
35
39
|
|
|
36
|
-
const SECRET_KEY =
|
|
40
|
+
const SECRET_KEY = signingSecretFromEnv();
|
|
37
41
|
|
|
38
42
|
// The credential a backend presents to validate a token. Defaults to the signing
|
|
39
43
|
// key, which is what the README has always documented, but can be separated:
|
|
@@ -1700,7 +1704,7 @@ app.post('/api/score', async (req, res) => {
|
|
|
1700
1704
|
});
|
|
1701
1705
|
|
|
1702
1706
|
app.post('/api/token/verify', (req, res) => {
|
|
1703
|
-
const { token, secret } = req.body;
|
|
1707
|
+
const { token, secret, remoteip } = req.body;
|
|
1704
1708
|
|
|
1705
1709
|
// The secret gate. This endpoint is the boundary between "a browser finished a
|
|
1706
1710
|
// challenge" and "my backend believes it", so it is server-to-server and needs
|
|
@@ -1715,10 +1719,10 @@ app.post('/api/token/verify', (req, res) => {
|
|
|
1715
1719
|
}
|
|
1716
1720
|
}
|
|
1717
1721
|
|
|
1718
|
-
//
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
res.json(verifyToken(token,
|
|
1722
|
+
// This request comes from the integrating backend, not from the visitor who
|
|
1723
|
+
// received the token. Bind only when that trusted backend explicitly supplies
|
|
1724
|
+
// the visitor address; using the caller socket here compares unrelated hosts.
|
|
1725
|
+
res.json(verifyToken(token, typeof remoteip === 'string' && remoteip ? remoteip : null));
|
|
1722
1726
|
});
|
|
1723
1727
|
|
|
1724
1728
|
// Turnstile / reCAPTCHA / hCaptcha drop-in compatibility.
|
|
@@ -1787,6 +1791,16 @@ app.get('/api/challenge', (req, res) => {
|
|
|
1787
1791
|
});
|
|
1788
1792
|
});
|
|
1789
1793
|
|
|
1794
|
+
// Express identifies parser limit failures with status 413 / entity.too.large.
|
|
1795
|
+
// Handle them explicitly so all three servers expose the same status instead
|
|
1796
|
+
// of falling through to Express's HTML error response.
|
|
1797
|
+
app.use((err, req, res, next) => {
|
|
1798
|
+
if (err && (err.status === 413 || err.type === 'entity.too.large')) {
|
|
1799
|
+
return res.status(413).json({ error: 'request_too_large' });
|
|
1800
|
+
}
|
|
1801
|
+
return next(err);
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1790
1804
|
// =============================================================================
|
|
1791
1805
|
// Start
|
|
1792
1806
|
// =============================================================================
|