@webdecoy/fcaptcha 1.0.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +61 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdecoy/fcaptcha",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Open source CAPTCHA with PoW, bot detection, and Vision AI protection",
5
5
  "main": "index.js",
6
6
  "exports": {
package/server.js CHANGED
@@ -171,6 +171,35 @@ const fingerprintStore = {
171
171
  }
172
172
  };
173
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
+
174
203
  // =============================================================================
175
204
  // Detection Patterns
176
205
  // =============================================================================
@@ -629,7 +658,7 @@ function generateToken(ip, siteKey, score) {
629
658
  return Buffer.from(JSON.stringify(data)).toString('base64url');
630
659
  }
631
660
 
632
- function verifyToken(token) {
661
+ function verifyToken(token, ip = null) {
633
662
  try {
634
663
  const decoded = JSON.parse(Buffer.from(token, 'base64url').toString());
635
664
 
@@ -648,11 +677,28 @@ function verifyToken(token) {
648
677
  return { valid: false, reason: 'invalid_signature' };
649
678
  }
650
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
+
651
696
  return {
652
697
  valid: true,
653
698
  site_key: decoded.site_key,
654
699
  timestamp: decoded.timestamp,
655
- score: decoded.score
700
+ score: decoded.score,
701
+ ip_hash: decoded.ip_hash
656
702
  };
657
703
  } catch (e) {
658
704
  return { valid: false, reason: e.message };
@@ -816,7 +862,19 @@ app.post('/api/score', (req, res) => {
816
862
 
817
863
  app.post('/api/token/verify', (req, res) => {
818
864
  const { token } = req.body;
819
- res.json(verifyToken(token));
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));
820
878
  });
821
879
 
822
880
  // PoW Challenge endpoint - client fetches this on page load